diff options
Diffstat (limited to 'platform')
143 files changed, 3659 insertions, 2236 deletions
diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h index 857bef02d1..a10afa1df8 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -31,130 +31,9 @@ #ifndef ANDROID_KEYS_UTILS_H #define ANDROID_KEYS_UTILS_H +#include <android/input.h> #include <core/os/keyboard.h> -/* - * Android Key codes. - */ -enum { - AKEYCODE_UNKNOWN = 0, - AKEYCODE_SOFT_LEFT = 1, - AKEYCODE_SOFT_RIGHT = 2, - AKEYCODE_HOME = 3, - AKEYCODE_BACK = 4, - AKEYCODE_CALL = 5, - AKEYCODE_ENDCALL = 6, - AKEYCODE_0 = 7, - AKEYCODE_1 = 8, - AKEYCODE_2 = 9, - AKEYCODE_3 = 10, - AKEYCODE_4 = 11, - AKEYCODE_5 = 12, - AKEYCODE_6 = 13, - AKEYCODE_7 = 14, - AKEYCODE_8 = 15, - AKEYCODE_9 = 16, - AKEYCODE_STAR = 17, - AKEYCODE_POUND = 18, - AKEYCODE_DPAD_UP = 19, - AKEYCODE_DPAD_DOWN = 20, - AKEYCODE_DPAD_LEFT = 21, - AKEYCODE_DPAD_RIGHT = 22, - AKEYCODE_DPAD_CENTER = 23, - AKEYCODE_VOLUME_UP = 24, - AKEYCODE_VOLUME_DOWN = 25, - AKEYCODE_POWER = 26, - AKEYCODE_CAMERA = 27, - AKEYCODE_CLEAR = 28, - AKEYCODE_A = 29, - AKEYCODE_B = 30, - AKEYCODE_C = 31, - AKEYCODE_D = 32, - AKEYCODE_E = 33, - AKEYCODE_F = 34, - AKEYCODE_G = 35, - AKEYCODE_H = 36, - AKEYCODE_I = 37, - AKEYCODE_J = 38, - AKEYCODE_K = 39, - AKEYCODE_L = 40, - AKEYCODE_M = 41, - AKEYCODE_N = 42, - AKEYCODE_O = 43, - AKEYCODE_P = 44, - AKEYCODE_Q = 45, - AKEYCODE_R = 46, - AKEYCODE_S = 47, - AKEYCODE_T = 48, - AKEYCODE_U = 49, - AKEYCODE_V = 50, - AKEYCODE_W = 51, - AKEYCODE_X = 52, - AKEYCODE_Y = 53, - AKEYCODE_Z = 54, - AKEYCODE_COMMA = 55, - AKEYCODE_PERIOD = 56, - AKEYCODE_ALT_LEFT = 57, - AKEYCODE_ALT_RIGHT = 58, - AKEYCODE_SHIFT_LEFT = 59, - AKEYCODE_SHIFT_RIGHT = 60, - AKEYCODE_TAB = 61, - AKEYCODE_SPACE = 62, - AKEYCODE_SYM = 63, - AKEYCODE_EXPLORER = 64, - AKEYCODE_ENVELOPE = 65, - AKEYCODE_ENTER = 66, - AKEYCODE_DEL = 67, - AKEYCODE_GRAVE = 68, - AKEYCODE_MINUS = 69, - AKEYCODE_EQUALS = 70, - AKEYCODE_LEFT_BRACKET = 71, - AKEYCODE_RIGHT_BRACKET = 72, - AKEYCODE_BACKSLASH = 73, - AKEYCODE_SEMICOLON = 74, - AKEYCODE_APOSTROPHE = 75, - AKEYCODE_SLASH = 76, - AKEYCODE_AT = 77, - AKEYCODE_NUM = 78, - AKEYCODE_HEADSETHOOK = 79, - AKEYCODE_FOCUS = 80, // *Camera* focus - AKEYCODE_PLUS = 81, - AKEYCODE_MENU = 82, - AKEYCODE_NOTIFICATION = 83, - AKEYCODE_SEARCH = 84, - AKEYCODE_MEDIA_PLAY_PAUSE = 85, - AKEYCODE_MEDIA_STOP = 86, - AKEYCODE_MEDIA_NEXT = 87, - AKEYCODE_MEDIA_PREVIOUS = 88, - AKEYCODE_MEDIA_REWIND = 89, - AKEYCODE_MEDIA_FAST_FORWARD = 90, - AKEYCODE_MUTE = 91, - AKEYCODE_PAGE_UP = 92, - AKEYCODE_PAGE_DOWN = 93, - AKEYCODE_PICTSYMBOLS = 94, - AKEYCODE_SWITCH_CHARSET = 95, - AKEYCODE_BUTTON_A = 96, - AKEYCODE_BUTTON_B = 97, - AKEYCODE_BUTTON_C = 98, - AKEYCODE_BUTTON_X = 99, - AKEYCODE_BUTTON_Y = 100, - AKEYCODE_BUTTON_Z = 101, - AKEYCODE_BUTTON_L1 = 102, - AKEYCODE_BUTTON_R1 = 103, - AKEYCODE_BUTTON_L2 = 104, - AKEYCODE_BUTTON_R2 = 105, - AKEYCODE_BUTTON_THUMBL = 106, - AKEYCODE_BUTTON_THUMBR = 107, - AKEYCODE_BUTTON_START = 108, - AKEYCODE_BUTTON_SELECT = 109, - AKEYCODE_BUTTON_MODE = 110, - AKEYCODE_CONTROL_LEFT = 113, - AKEYCODE_CONTROL_RIGHT = 114, - - // NOTE: If you add a new keycode here you must also add it to several other files. - // Refer to frameworks/base/core/java/android/view/KeyEvent.java for the full list. -}; - struct _WinTranslatePair { unsigned int keysym; unsigned int keycode; @@ -248,8 +127,8 @@ static _WinTranslatePair _ak_to_keycode[] = { { KEY_BACKSLASH, AKEYCODE_BACKSLASH }, { KEY_BRACKETLEFT, AKEYCODE_LEFT_BRACKET }, { KEY_BRACKETRIGHT, AKEYCODE_RIGHT_BRACKET }, - { KEY_CONTROL, AKEYCODE_CONTROL_LEFT }, - { KEY_CONTROL, AKEYCODE_CONTROL_RIGHT }, + { KEY_CONTROL, AKEYCODE_CTRL_LEFT }, + { KEY_CONTROL, AKEYCODE_CTRL_RIGHT }, { KEY_UNKNOWN, 0 } }; /* diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 1f140f7119..8b82733d2d 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -30,7 +30,7 @@ #include "api.h" -#include "core/engine.h" +#include "core/config/engine.h" #include "java_class_wrapper.h" #include "jni_singleton.h" diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index 1fa2726784..4718de29ad 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -31,7 +31,7 @@ #ifndef JAVA_CLASS_WRAPPER_H #define JAVA_CLASS_WRAPPER_H -#include "core/reference.h" +#include "core/object/reference.h" #ifdef ANDROID_ENABLED #include <android/log.h> diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 5e63f20d6c..7f20c354a0 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -31,8 +31,8 @@ #ifndef JNI_SINGLETON_H #define JNI_SINGLETON_H -#include <core/engine.h> -#include <core/variant.h> +#include <core/config/engine.h> +#include <core/variant/variant.h> #ifdef ANDROID_ENABLED #include <platform/android/jni_utils.h> #endif diff --git a/platform/android/audio_driver_jandroid.cpp b/platform/android/audio_driver_jandroid.cpp index 09c981b3fa..1363c5ac1e 100644 --- a/platform/android/audio_driver_jandroid.cpp +++ b/platform/android/audio_driver_jandroid.cpp @@ -30,8 +30,8 @@ #include "audio_driver_jandroid.h" +#include "core/config/project_settings.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "thread_jandroid.h" AudioDriverAndroid *AudioDriverAndroid::s_ad = nullptr; diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index ca312b427f..ba75a4b10c 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -29,7 +29,7 @@ /*************************************************************************/ #include "dir_access_jandroid.h" -#include "core/print_string.h" +#include "core/string/print_string.h" #include "file_access_jandroid.h" #include "string_android.h" #include "thread_jandroid.h" diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 235c9ff665..e82a12ece5 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -31,14 +31,13 @@ #include "display_server_android.h" #include "android_keys_utils.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif +#include <android/input.h> + #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" #include "platform/android/vulkan/vulkan_context_android.h" @@ -58,7 +57,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { //case FEATURE_HIDPI: //case FEATURE_ICON: //case FEATURE_IME: - //case FEATURE_MOUSE: + case FEATURE_MOUSE: //case FEATURE_MOUSE_WARP: //case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_ICON: @@ -140,8 +139,11 @@ Size2i DisplayServerAndroid::screen_get_size(int p_screen) const { } Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const { - Size2i display_size = OS_Android::get_singleton()->get_display_size(); - return Rect2i(0, 0, display_size.width, display_size.height); + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_COND_V(!godot_io_java, Rect2i()); + int xywh[4]; + godot_io_java->screen_get_usable_rect(xywh); + return Rect2i(xywh[0], xywh[1], xywh[2], xywh[3]); } int DisplayServerAndroid::screen_get_dpi(int p_screen) const { @@ -343,7 +345,7 @@ void DisplayServerAndroid::alert(const String &p_alert, const String &p_title) { } void DisplayServerAndroid::process_events() { - // Nothing to do + Input::get_singleton()->flush_accumulated_events(); } Vector<String> DisplayServerAndroid::get_rendering_drivers_func() { @@ -398,6 +400,8 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on"); + buttons_state = 0; + #if defined(OPENGL_ENABLED) if (rendering_driver == "opengl") { bool gl_initialization_error = false; @@ -532,12 +536,12 @@ void DisplayServerAndroid::process_key_event(int p_keycode, int p_scancode, int OS_Android::get_singleton()->main_loop_request_go_back(); } - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); } -void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector<DisplayServerAndroid::TouchPos> &p_points) { - switch (p_what) { - case 0: { //gesture begin +void DisplayServerAndroid::process_touch(int p_event, int p_pointer, const Vector<DisplayServerAndroid::TouchPos> &p_points) { + switch (p_event) { + case AMOTION_EVENT_ACTION_DOWN: { //gesture begin if (touch.size()) { //end all if exist for (int i = 0; i < touch.size(); i++) { @@ -546,7 +550,7 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(touch[i].id); ev->set_pressed(false); ev->set_position(touch[i].pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); } } @@ -563,11 +567,11 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(touch[i].id); ev->set_pressed(true); ev->set_position(touch[i].pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); } } break; - case 1: { //motion + case AMOTION_EVENT_ACTION_MOVE: { //motion ERR_FAIL_COND(touch.size() != p_points.size()); for (int i = 0; i < touch.size(); i++) { @@ -589,12 +593,13 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(touch[i].id); ev->set_position(p_points[idx].pos); ev->set_relative(p_points[idx].pos - touch[i].pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); touch.write[i].pos = p_points[idx].pos; } } break; - case 2: { //release + case AMOTION_EVENT_ACTION_CANCEL: + case AMOTION_EVENT_ACTION_UP: { //release if (touch.size()) { //end all if exist for (int i = 0; i < touch.size(); i++) { @@ -603,12 +608,12 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(touch[i].id); ev->set_pressed(false); ev->set_position(touch[i].pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); } touch.clear(); } } break; - case 3: { // add touch + case AMOTION_EVENT_ACTION_POINTER_DOWN: { // add touch for (int i = 0; i < p_points.size(); i++) { if (p_points[i].id == p_pointer) { TouchPos tp = p_points[i]; @@ -620,13 +625,13 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(tp.id); ev->set_pressed(true); ev->set_position(tp.pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); break; } } } break; - case 4: { // remove touch + case AMOTION_EVENT_ACTION_POINTER_UP: { // remove touch for (int i = 0; i < touch.size(); i++) { if (touch[i].id == p_pointer) { Ref<InputEventScreenTouch> ev; @@ -634,7 +639,7 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector ev->set_index(touch[i].id); ev->set_pressed(false); ev->set_position(touch[i].pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); touch.remove(i); break; @@ -647,30 +652,116 @@ void DisplayServerAndroid::process_touch(int p_what, int p_pointer, const Vector void DisplayServerAndroid::process_hover(int p_type, Point2 p_pos) { // https://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER switch (p_type) { - case 7: // hover move - case 9: // hover enter - case 10: { // hover exit + case AMOTION_EVENT_ACTION_HOVER_MOVE: // hover move + case AMOTION_EVENT_ACTION_HOVER_ENTER: // hover enter + case AMOTION_EVENT_ACTION_HOVER_EXIT: { // hover exit Ref<InputEventMouseMotion> ev; ev.instance(); _set_key_modifier_state(ev); ev->set_position(p_pos); ev->set_global_position(p_pos); ev->set_relative(p_pos - hover_prev_pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); hover_prev_pos = p_pos; } break; } } -void DisplayServerAndroid::process_double_tap(Point2 p_pos) { +void DisplayServerAndroid::process_mouse_event(int event_action, int event_android_buttons_mask, Point2 event_pos, float event_vertical_factor, float event_horizontal_factor) { + int event_buttons_mask = _android_button_mask_to_godot_button_mask(event_android_buttons_mask); + switch (event_action) { + case AMOTION_EVENT_ACTION_BUTTON_PRESS: + case AMOTION_EVENT_ACTION_BUTTON_RELEASE: { + Ref<InputEventMouseButton> ev; + ev.instance(); + _set_key_modifier_state(ev); + ev->set_position(event_pos); + ev->set_global_position(event_pos); + ev->set_pressed(event_action == AMOTION_EVENT_ACTION_BUTTON_PRESS); + int changed_button_mask = buttons_state ^ event_buttons_mask; + + buttons_state = event_buttons_mask; + + ev->set_button_index(_button_index_from_mask(changed_button_mask)); + ev->set_button_mask(event_buttons_mask); + Input::get_singleton()->accumulate_input_event(ev); + } break; + + case AMOTION_EVENT_ACTION_MOVE: { + Ref<InputEventMouseMotion> ev; + ev.instance(); + _set_key_modifier_state(ev); + ev->set_position(event_pos); + ev->set_global_position(event_pos); + ev->set_relative(event_pos - hover_prev_pos); + ev->set_button_mask(event_buttons_mask); + Input::get_singleton()->accumulate_input_event(ev); + hover_prev_pos = event_pos; + } break; + case AMOTION_EVENT_ACTION_SCROLL: { + Ref<InputEventMouseButton> ev; + ev.instance(); + ev->set_position(event_pos); + ev->set_global_position(event_pos); + ev->set_pressed(true); + buttons_state = event_buttons_mask; + if (event_vertical_factor > 0) { + _wheel_button_click(event_buttons_mask, ev, BUTTON_WHEEL_UP, event_vertical_factor); + } else if (event_vertical_factor < 0) { + _wheel_button_click(event_buttons_mask, ev, BUTTON_WHEEL_DOWN, -event_vertical_factor); + } + + if (event_horizontal_factor > 0) { + _wheel_button_click(event_buttons_mask, ev, BUTTON_WHEEL_RIGHT, event_horizontal_factor); + } else if (event_horizontal_factor < 0) { + _wheel_button_click(event_buttons_mask, ev, BUTTON_WHEEL_LEFT, -event_horizontal_factor); + } + } break; + } +} + +void DisplayServerAndroid::_wheel_button_click(int event_buttons_mask, const Ref<InputEventMouseButton> &ev, int wheel_button, float factor) { + Ref<InputEventMouseButton> evd = ev->duplicate(); + _set_key_modifier_state(evd); + evd->set_button_index(wheel_button); + evd->set_button_mask(event_buttons_mask ^ (1 << (wheel_button - 1))); + evd->set_factor(factor); + Input::get_singleton()->accumulate_input_event(evd); + Ref<InputEventMouseButton> evdd = evd->duplicate(); + evdd->set_pressed(false); + evdd->set_button_mask(event_buttons_mask); + Input::get_singleton()->accumulate_input_event(evdd); +} + +void DisplayServerAndroid::process_double_tap(int event_android_button_mask, Point2 p_pos) { + int event_button_mask = _android_button_mask_to_godot_button_mask(event_android_button_mask); Ref<InputEventMouseButton> ev; ev.instance(); _set_key_modifier_state(ev); ev->set_position(p_pos); ev->set_global_position(p_pos); - ev->set_pressed(false); + ev->set_pressed(event_button_mask != 0); + ev->set_button_index(_button_index_from_mask(event_button_mask)); + ev->set_button_mask(event_button_mask); ev->set_doubleclick(true); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); +} + +int DisplayServerAndroid::_button_index_from_mask(int button_mask) { + switch (button_mask) { + case BUTTON_MASK_LEFT: + return BUTTON_LEFT; + case BUTTON_MASK_RIGHT: + return BUTTON_RIGHT; + case BUTTON_MASK_MIDDLE: + return BUTTON_MIDDLE; + case BUTTON_MASK_XBUTTON1: + return BUTTON_XBUTTON1; + case BUTTON_MASK_XBUTTON2: + return BUTTON_XBUTTON2; + default: + return 0; + } } void DisplayServerAndroid::process_scroll(Point2 p_pos) { @@ -679,7 +770,7 @@ void DisplayServerAndroid::process_scroll(Point2 p_pos) { _set_key_modifier_state(ev); ev->set_position(p_pos); ev->set_delta(p_pos - scroll_prev_pos); - Input::get_singleton()->parse_input_event(ev); + Input::get_singleton()->accumulate_input_event(ev); scroll_prev_pos = p_pos; } @@ -698,3 +789,32 @@ void DisplayServerAndroid::process_magnetometer(const Vector3 &p_magnetometer) { void DisplayServerAndroid::process_gyroscope(const Vector3 &p_gyroscope) { Input::get_singleton()->set_gyroscope(p_gyroscope); } + +Point2i DisplayServerAndroid::mouse_get_position() const { + return hover_prev_pos; +} + +int DisplayServerAndroid::mouse_get_button_state() const { + return buttons_state; +} + +int DisplayServerAndroid::_android_button_mask_to_godot_button_mask(int android_button_mask) { + int godot_button_mask = 0; + if (android_button_mask & AMOTION_EVENT_BUTTON_PRIMARY) { + godot_button_mask |= BUTTON_MASK_LEFT; + } + if (android_button_mask & AMOTION_EVENT_BUTTON_SECONDARY) { + godot_button_mask |= BUTTON_MASK_RIGHT; + } + if (android_button_mask & AMOTION_EVENT_BUTTON_TERTIARY) { + godot_button_mask |= BUTTON_MASK_MIDDLE; + } + if (android_button_mask & AMOTION_EVENT_BUTTON_BACK) { + godot_button_mask |= BUTTON_MASK_XBUTTON1; + } + if (android_button_mask & AMOTION_EVENT_BUTTON_SECONDARY) { + godot_button_mask |= BUTTON_MASK_XBUTTON2; + } + + return godot_button_mask; +} diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 5cdc69ee83..aa5a2c1185 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -68,6 +68,8 @@ private: bool control_mem = false; bool meta_mem = false; + int buttons_state; + bool keep_screen_on; Vector<TouchPos> touch; @@ -91,6 +93,12 @@ private: void _set_key_modifier_state(Ref<InputEventWithModifiers> ev); + static int _button_index_from_mask(int button_mask); + + static int _android_button_mask_to_godot_button_mask(int android_button_mask); + + void _wheel_button_click(int event_buttons_mask, const Ref<InputEventMouseButton> &ev, int wheel_button, float factor); + public: static DisplayServerAndroid *get_singleton(); @@ -162,9 +170,10 @@ public: void process_gravity(const Vector3 &p_gravity); void process_magnetometer(const Vector3 &p_magnetometer); void process_gyroscope(const Vector3 &p_gyroscope); - void process_touch(int p_what, int p_pointer, const Vector<TouchPos> &p_points); + void process_touch(int p_event, int p_pointer, const Vector<TouchPos> &p_points); void process_hover(int p_type, Point2 p_pos); - void process_double_tap(Point2 p_pos); + void process_mouse_event(int event_action, int event_android_buttons_mask, Point2 event_pos, float event_vertical_factor = 0, float event_horizontal_factor = 0); + void process_double_tap(int event_android_button_mask, Point2 p_pos); void process_scroll(Point2 p_pos); void process_joy_event(JoypadEvent p_event); void process_key_event(int p_keycode, int p_scancode, int p_unicode_char, bool p_pressed); @@ -175,6 +184,9 @@ public: void reset_window(); + virtual Point2i mouse_get_position() const; + virtual int mouse_get_button_state() const; + DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); ~DisplayServerAndroid(); }; diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 8d3257a365..5007b3f570 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -31,19 +31,20 @@ #include "export.h" #include "gradle_export_util.h" +#include "core/config/project_settings.h" #include "core/io/image_loader.h" #include "core/io/marshalls.h" #include "core/io/zip_io.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "core/version.h" #include "drivers/png/png_driver_common.h" #include "editor/editor_export.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "main/splash.gen.h" #include "platform/android/export/gradle_export_util.h" #include "platform/android/logo.gen.h" #include "platform/android/plugin/godot_plugin_config.h" @@ -200,6 +201,9 @@ static const char *android_perms[] = { nullptr }; +static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable/splash.png"; +static const char *SPLASH_BG_COLOR_PATH = "res/drawable/splash_bg_color.png"; + struct LauncherIcon { const char *export_path; int dimensions; @@ -453,7 +457,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String name; bool first = true; for (int i = 0; i < basename.length(); i++) { - CharType c = basename[i]; + char32_t c = basename[i]; if (c >= '0' && c <= '9' && first) { continue; } @@ -484,7 +488,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { int segments = 0; bool first = true; for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (first && c == '.') { if (r_error) { *r_error = TTR("Package segments must be of non-zero length."); @@ -588,7 +592,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { zipfi.tmz_date.tm_hour = time.hour; zipfi.tmz_date.tm_mday = date.day; zipfi.tmz_date.tm_min = time.min; - zipfi.tmz_date.tm_mon = date.month; + zipfi.tmz_date.tm_mon = date.month - 1; // tm_mon is zero indexed zipfi.tmz_date.tm_sec = time.sec; zipfi.tmz_date.tm_year = date.year; zipfi.dosDate = 0; @@ -723,7 +727,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { APKExportData *ed = (APKExportData *)p_userdata; String dst_path = p_path.replace_first("res://", "assets/"); @@ -731,7 +735,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { return OK; } @@ -873,7 +877,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { if (string_flags & UTF8_FLAG) { } else { uint32_t len = decode_uint16(&p_manifest[string_at]); - Vector<CharType> ucstring; + Vector<char32_t> ucstring; ucstring.resize(len + 1); for (uint32_t j = 0; j < len; j++) { uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]); @@ -1334,7 +1338,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } else { String str; for (uint32_t i = 0; i < len; i++) { - CharType c = decode_uint16(&p_bytes[offset + i * 2]); + char32_t c = decode_uint16(&p_bytes[offset + i * 2]); if (c == 0) { break; } @@ -1433,6 +1437,18 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { //printf("end\n"); } + void _load_image_data(const Ref<Image> &p_splash_image, Vector<uint8_t> &p_data) { + Vector<uint8_t> png_buffer; + Error err = PNGDriverCommon::image_to_png(p_splash_image, png_buffer); + if (err == OK) { + p_data.resize(png_buffer.size()); + memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size()); + } else { + String err_str = String("Failed to convert splash image to png."); + WARN_PRINT(err_str.utf8().get_data()); + } + } + void _process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) { Ref<Image> working_image = p_source_image; @@ -1452,6 +1468,35 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } } + void load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) { + // TODO: Figure out how to handle remaining boot splash parameters (e.g: fullsize, filter) + String project_splash_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + + if (!project_splash_path.empty()) { + splash_image.instance(); + const Error err = ImageLoader::load_image(project_splash_path, splash_image); + if (err) { + splash_image.unref(); + } + } + + if (splash_image.is_null()) { + // Use the default + splash_image = Ref<Image>(memnew(Image(boot_splash_png))); + } + + // Setup the splash bg color + bool bg_color_valid; + Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid); + if (!bg_color_valid) { + bg_color = boot_splash_bg_color; + } + + splash_bg_color_image.instance(); + splash_bg_color_image->create(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); + splash_bg_color_image->fill(bg_color); + } + void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background) { String project_icon_path = ProjectSettings::get_singleton()->get("application/config/icon"); @@ -1479,13 +1524,34 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) { - String img_path = launcher_icon.export_path; - img_path = img_path.insert(0, "res://android/build/"); + store_image(launcher_icon.export_path, data); + } + + void store_image(const String &export_path, const Vector<uint8_t> &data) { + String img_path = export_path.insert(0, "res://android/build/"); store_file_at_path(img_path, data); } - void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &main_image, - const Ref<Image> &foreground, const Ref<Image> &background) { + void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, + const Ref<Image> &splash_image, + const Ref<Image> &splash_bg_color_image, + const Ref<Image> &main_image, + const Ref<Image> &foreground, + const Ref<Image> &background) { + // Store the splash image + if (splash_image.is_valid() && !splash_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_image, data); + store_image(SPLASH_IMAGE_EXPORT_PATH, data); + } + + // Store the splash bg color image + if (splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_bg_color_image, data); + store_image(SPLASH_BG_COLOR_PATH, data); + } + // Prepare images to be resized for the icons. If some image ends up being uninitialized, // the default image from the export template will be used. @@ -1525,7 +1591,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } public: - typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total); + typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override { @@ -1554,6 +1620,7 @@ public: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_template/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), 0)); Vector<PluginConfig> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { @@ -1974,6 +2041,13 @@ public: } } + if (int(p_preset->get("custom_template/export_format")) == 1 && /*AAB*/ + !bool(p_preset->get("custom_template/use_custom_build"))) { + valid = false; + err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled."); + err += "\n"; + } + r_error = err; return valid; } @@ -1981,6 +2055,7 @@ public: virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { List<String> list; list.push_back("apk"); + list.push_back("aab"); return list; } @@ -2007,7 +2082,21 @@ public: return have_plugins_changed || first_build; } - Error get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { + String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + int version_code = p_preset->get("version/code"); + String package_name = p_preset->get("package/unique_name"); + String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb"; + String fullpath = p_path.get_base_dir().plus_file(apk_file_name); + return fullpath; + } + + Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + String fullpath = get_apk_expansion_fullpath(p_preset, p_path); + Error err = save_pack(p_preset, fullpath); + return err; + } + + void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { String cmdline = p_preset->get("command_line/extra_args"); Vector<String> command_line_strings = cmdline.strip_edges().split(" "); for (int i = 0; i < command_line_strings.size(); i++) { @@ -2021,17 +2110,8 @@ public: bool apk_expansion = p_preset->get("apk_expansion/enable"); if (apk_expansion) { - int version_code = p_preset->get("version/code"); - String package_name = p_preset->get("package/unique_name"); - String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb"; - String fullpath = p_path.get_base_dir().plus_file(apk_file_name); + String fullpath = get_apk_expansion_fullpath(p_preset, p_path); String apk_expansion_public_key = p_preset->get("apk_expansion/public_key"); - Error err = save_pack(p_preset, fullpath); - - if (err != OK) { - EditorNode::add_io_error("Could not write expansion package file: " + apk_file_name); - return err; - } command_line_strings.push_back("--use_apk_expansion"); command_line_strings.push_back("--apk_expansion_md5"); @@ -2077,6 +2157,92 @@ public: copymem(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length); } } + } + + Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, String apk_path, EditorProgress ep) { + String release_keystore = p_preset->get("keystore/release"); + String release_username = p_preset->get("keystore/release_user"); + String release_password = p_preset->get("keystore/release_password"); + + String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); + if (!FileAccess::exists(jarsigner)) { + EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); + return OK; + } + + String keystore; + String password; + String user; + if (p_debug) { + keystore = p_preset->get("keystore/debug"); + password = p_preset->get("keystore/debug_password"); + user = p_preset->get("keystore/debug_user"); + + if (keystore.empty()) { + keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); + password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); + user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); + } + + if (ep.step("Signing debug APK...", 103)) { + return ERR_SKIP; + } + + } else { + keystore = release_keystore; + password = release_password; + user = release_username; + + if (ep.step("Signing release APK...", 103)) { + return ERR_SKIP; + } + } + + if (!FileAccess::exists(keystore)) { + EditorNode::add_io_error("Could not find keystore, unable to export."); + return ERR_FILE_CANT_OPEN; + } + + List<String> args; + args.push_back("-digestalg"); + args.push_back("SHA-256"); + args.push_back("-sigalg"); + args.push_back("SHA256withRSA"); + String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); + if (tsa_url != "") { + args.push_back("-tsa"); + args.push_back(tsa_url); + } + args.push_back("-verbose"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back("-storepass"); + args.push_back(password); + args.push_back(apk_path); + args.push_back(user); + int retval; + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); + return ERR_CANT_CREATE; + } + + if (ep.step("Verifying APK...", 104)) { + return ERR_SKIP; + } + + args.clear(); + args.push_back("-verify"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back(apk_path); + args.push_back("-verbose"); + + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); + return ERR_CANT_CREATE; + } return OK; } @@ -2084,11 +2250,20 @@ public: ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); String src_apk; + Error err; EditorProgress ep("export", "Exporting for Android", 105, true); bool use_custom_build = bool(p_preset->get("custom_template/use_custom_build")); + int export_format = int(p_preset->get("custom_template/export_format")); bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); + bool _signed = p_preset->get("package/signed"); + bool apk_expansion = p_preset->get("apk_expansion/enable"); + Vector<String> enabled_abis = get_enabled_abis(p_preset); + + Ref<Image> splash_image; + Ref<Image> splash_bg_color_image; + load_splash_refs(splash_image, splash_bg_color_image); Ref<Image> main_image; Ref<Image> foreground; @@ -2096,40 +2271,84 @@ public: load_icon_refs(p_preset, main_image, foreground, background); + Vector<uint8_t> command_line_flags; + // Write command line flags into the command_line_flags variable. + get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); + + if (export_format == 1) { + if (!p_path.ends_with(".aab")) { + EditorNode::get_singleton()->show_warning(TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); + return ERR_UNCONFIGURED; + } + if (apk_expansion) { + EditorNode::get_singleton()->show_warning(TTR("APK Expansion not compatible with Android App Bundle.")); + return ERR_UNCONFIGURED; + } + } + if (export_format == 0 && !p_path.ends_with(".apk")) { + EditorNode::get_singleton()->show_warning( + TTR("Invalid filename! Android APK requires the *.apk extension.")); + return ERR_UNCONFIGURED; + } + if (export_format > 1 || export_format < 0) { + EditorNode::add_io_error("Unsupported export format!\n"); + return ERR_UNCONFIGURED; //TODO: is this the right error? + } + if (use_custom_build) { - //re-generate build.gradle and AndroidManifest.xml - { //test that installed build version is alright + //test that installed build version is alright + { FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); if (!f) { EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String version = f->get_line().strip_edges(); + f->close(); if (version != VERSION_FULL_CONFIG) { EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); return ERR_UNCONFIGURED; } } + String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); + ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); // TODO: should we use "package/name" or "application/config/name"? String project_name = get_project_name(p_preset->get("package/name")); - // instead of calling _fix_resources - Error err = _create_project_name_strings_files(p_preset, project_name); + err = _create_project_name_strings_files(p_preset, project_name); //project name localization. if (err != OK) { EditorNode::add_io_error("Unable to overwrite res://android/build/res/*.xml files with project name"); } // Copies the project icon files into the appropriate Gradle project directory. - _copy_icons_to_gradle_project(p_preset, main_image, foreground, background); + _copy_icons_to_gradle_project(p_preset, splash_image, splash_bg_color_image, main_image, foreground, background); // Write an AndroidManifest.xml file into the Gradle project directory. _write_tmp_manifest(p_preset, p_give_internet, p_debug); - //build project if custom build is enabled - String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); - ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); + //stores all the project files inside the Gradle project directory. Also includes all ABIs + if (!apk_expansion) { + DirAccess *da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (da_res->dir_exists("res://android/build/assets")) { + DirAccess *da_assets = DirAccess::open("res://android/build/assets"); + da_assets->erase_contents_recursive(); + da_res->remove("res://android/build/assets"); + } + err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, NULL, ignore_so_file); + if (err != OK) { + EditorNode::add_io_error("Could not export project files to gradle project\n"); + return err; + } + } else { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } + store_file_at_path("res://android/build/assets/_cl_", command_line_flags); OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required - String build_command; + #ifdef WINDOWS_ENABLED build_command = "gradlew.bat"; #else @@ -2137,12 +2356,12 @@ public: #endif String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build"); - build_command = build_path.plus_file(build_command); String package_name = get_package_name(p_preset->get("package/unique_name")); String version_code = itos(p_preset->get("version/code")); String version_name = p_preset->get("version/name"); + String enabled_abi_string = String("|").join(enabled_abis); Vector<PluginConfig> enabled_plugins = get_enabled_plugins(p_preset); String local_plugins_binaries = get_plugins_binaries(BINARY_TYPE_LOCAL, enabled_plugins); @@ -2154,10 +2373,20 @@ public: if (clean_build_required) { cmdline.push_back("clean"); } - cmdline.push_back("build"); + + String build_type = p_debug ? "Debug" : "Release"; + if (export_format == 1) { + String bundle_build_command = vformat("bundle%s", build_type); + cmdline.push_back(bundle_build_command); + } else if (export_format == 0) { + String apk_build_command = vformat("assemble%s", build_type); + cmdline.push_back(apk_build_command); + } + cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name. + cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs. cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies. cmdline.push_back("-Pplugins_remote_binaries=" + remote_plugins_binaries); // argument to specify the list of plugins remote dependencies. cmdline.push_back("-Pplugins_maven_repos=" + custom_maven_repos); // argument to specify the list of custom maven repos for the plugins dependencies. @@ -2175,35 +2404,54 @@ public: EditorNode::get_singleton()->show_warning(TTR("Building of Android project failed, check output for the error.\nAlternatively visit docs.godotengine.org for Android build documentation.")); return ERR_CANT_CREATE; } - if (p_debug) { - src_apk = build_path.plus_file("build/outputs/apk/debug/android_debug.apk"); - } else { - src_apk = build_path.plus_file("build/outputs/apk/release/android_release.apk"); + + List<String> copy_args; + String copy_command; + if (export_format == 1) { + copy_command = vformat("copyAndRename%sAab", build_type); + } else if (export_format == 0) { + copy_command = vformat("copyAndRename%sApk", build_type); } - if (!FileAccess::exists(src_apk)) { - EditorNode::get_singleton()->show_warning(TTR("No build apk generated at: ") + "\n" + src_apk); + copy_args.push_back(copy_command); + + copy_args.push_back("-p"); // argument to specify the start directory. + copy_args.push_back(build_path); // start directory. + + String export_filename = p_path.get_file(); + String export_path = p_path.get_base_dir(); + + copy_args.push_back("-Pexport_path=file:" + export_path); + copy_args.push_back("-Pexport_filename=" + export_filename); + + int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args); + if (copy_result != 0) { + EditorNode::get_singleton()->show_warning(TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); return ERR_CANT_CREATE; } - - } else { + if (_signed) { + err = sign_apk(p_preset, p_debug, p_path, ep); + if (err != OK) { + return err; + } + } + return OK; + } + // This is the start of the Legacy build system + if (p_debug) + src_apk = p_preset->get("custom_template/debug"); + else + src_apk = p_preset->get("custom_template/release"); + src_apk = src_apk.strip_edges(); + if (src_apk == "") { if (p_debug) { - src_apk = p_preset->get("custom_template/debug"); + src_apk = find_export_template("android_debug.apk"); } else { - src_apk = p_preset->get("custom_template/release"); + src_apk = find_export_template("android_release.apk"); } - - src_apk = src_apk.strip_edges(); if (src_apk == "") { - if (p_debug) { - src_apk = find_export_template("android_debug.apk"); - } else { - src_apk = find_export_template("android_release.apk"); - } - if (src_apk == "") { - EditorNode::add_io_error("Package not found: " + src_apk); - return ERR_FILE_NOT_FOUND; - } + EditorNode::add_io_error("Package not found: " + src_apk); + return ERR_FILE_NOT_FOUND; } } @@ -2240,23 +2488,13 @@ public: zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); - bool _signed = p_preset->get("package/signed"); String cmdline = p_preset->get("command_line/extra_args"); String version_name = p_preset->get("version/name"); String package_name = p_preset->get("package/unique_name"); - bool apk_expansion = p_preset->get("apk_expansion/enable"); String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); - String release_keystore = p_preset->get("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); - - Vector<String> enabled_abis = get_enabled_abis(p_preset); - - // Prepare images to be resized for the icons. If some image ends up being uninitialized, the default image from the export template will be used. - Vector<String> invalid_abis(enabled_abis); while (ret == UNZ_OK) { //get filename @@ -2277,30 +2515,37 @@ public: unzCloseCurrentFile(pkg); //write + if (file == "AndroidManifest.xml") { + _fix_manifest(p_preset, data, p_give_internet); + } + if (file == "resources.arsc") { + _fix_resources(p_preset, data); + } - if (!use_custom_build) { - if (file == "AndroidManifest.xml") { - _fix_manifest(p_preset, data, p_give_internet); - } - if (file == "resources.arsc") { - _fix_resources(p_preset, data); - } + // Process the splash image + if (file == SPLASH_IMAGE_EXPORT_PATH && splash_image.is_valid() && !splash_image->empty()) { + _load_image_data(splash_image, data); + } - for (int i = 0; i < icon_densities_count; ++i) { - if (main_image.is_valid() && !main_image->empty()) { - if (file == launcher_icons[i].export_path) { - _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); - } + // Process the splash bg color image + if (file == SPLASH_BG_COLOR_PATH && splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + _load_image_data(splash_bg_color_image, data); + } + + for (int i = 0; i < icon_densities_count; ++i) { + if (main_image.is_valid() && !main_image->empty()) { + if (file == launcher_icons[i].export_path) { + _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); } - if (foreground.is_valid() && !foreground->empty()) { - if (file == launcher_adaptive_icon_foregrounds[i].export_path) { - _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); - } + } + if (foreground.is_valid() && !foreground->empty()) { + if (file == launcher_adaptive_icon_foregrounds[i].export_path) { + _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); } - if (background.is_valid() && !background->empty()) { - if (file == launcher_adaptive_icon_backgrounds[i].export_path) { - _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); - } + } + if (background.is_valid() && !background->empty()) { + if (file == launcher_adaptive_icon_backgrounds[i].export_path) { + _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); } } } @@ -2359,18 +2604,26 @@ public: if (ep.step("Adding files...", 1)) { CLEANUP_AND_RETURN(ERR_SKIP); } - Error err = OK; + err = OK; if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; err = export_project_files(p_preset, ignore_apk_file, &ed, save_apk_so); - } else if (!apk_expansion) { - APKExportData ed; - ed.ep = &ep; - ed.apk = unaligned_apk; - err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } else { + if (apk_expansion) { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } else { + APKExportData ed; + ed.ep = &ep; + ed.apk = unaligned_apk; + err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } } if (err != OK) { @@ -2379,10 +2632,6 @@ public: CLEANUP_AND_RETURN(ERR_SKIP); } - Vector<uint8_t> command_line_flags; - // Write command line flags into the command_line_flags variable. - err = get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); - zip_fileinfo zipfi = get_zip_fileinfo(); zipOpenNewFileInZip(unaligned_apk, "assets/_cl_", @@ -2404,84 +2653,9 @@ public: } if (_signed) { - String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); - if (!FileAccess::exists(jarsigner)) { - EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); - CLEANUP_AND_RETURN(OK); - } - - String keystore; - String password; - String user; - if (p_debug) { - keystore = p_preset->get("keystore/debug"); - password = p_preset->get("keystore/debug_password"); - user = p_preset->get("keystore/debug_user"); - - if (keystore.empty()) { - keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); - password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); - user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); - } - - if (ep.step("Signing debug APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - } else { - keystore = release_keystore; - password = release_password; - user = release_username; - - if (ep.step("Signing release APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - } - - if (!FileAccess::exists(keystore)) { - EditorNode::add_io_error("Could not find keystore, unable to export."); - CLEANUP_AND_RETURN(ERR_FILE_CANT_OPEN); - } - - List<String> args; - args.push_back("-digestalg"); - args.push_back("SHA-256"); - args.push_back("-sigalg"); - args.push_back("SHA256withRSA"); - String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); - if (tsa_url != "") { - args.push_back("-tsa"); - args.push_back(tsa_url); - } - args.push_back("-verbose"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back("-storepass"); - args.push_back(password); - args.push_back(tmp_unaligned_path); - args.push_back(user); - int retval; - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); - } - - if (ep.step("Verifying APK...", 104)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - args.clear(); - args.push_back("-verify"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back(tmp_unaligned_path); - args.push_back("-verbose"); - - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); + err = sign_apk(p_preset, p_debug, tmp_unaligned_path, ep); + if (err != OK) { + CLEANUP_AND_RETURN(err); } } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 209a664f8f..95f870bc35 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -99,7 +99,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. -Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { +Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { String dst_path = p_path.replace_first("res://", "res://android/build/assets/"); Error err = store_file_at_path(dst_path, p_data); return err; diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index 05d5fb576d..11faeff3e8 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -29,7 +29,7 @@ /*************************************************************************/ #include "file_access_android.h" -#include "core/print_string.h" +#include "core/string/print_string.h" AAssetManager *FileAccessAndroid::asset_manager = nullptr; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 48c09552c1..e94681659c 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -38,7 +38,7 @@ <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" - android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:theme="@style/GodotAppSplashTheme" android:launchMode="singleTask" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 3f8d138e8f..6de1d2dd30 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -70,8 +70,8 @@ android { buildToolsVersion versions.buildTools compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion } defaultConfig { @@ -80,6 +80,11 @@ android { ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~" } + ndk { + String[] export_abi_list = getExportEnabledABIs() + abiFilters export_abi_list + } + // Feel free to modify the application id to your own. applicationId getExportPackageName() versionCode getExportVersionCode() @@ -125,3 +130,27 @@ android { } } } + +task copyAndRenameDebugApk(type: Copy) { + from "$buildDir/outputs/apk/debug/android_debug.apk" + into getExportPath() + rename "android_debug.apk", getExportFilename() +} + +task copyAndRenameReleaseApk(type: Copy) { + from "$buildDir/outputs/apk/release/android_release.apk" + into getExportPath() + rename "android_release.apk", getExportFilename() +} + +task copyAndRenameDebugAab(type: Copy) { + from "$buildDir/outputs/bundle/debug/build-debug.aab" + into getExportPath() + rename "build-debug.aab", getExportFilename() +} + +task copyAndRenameReleaseAab(type: Copy) { + from "$buildDir/outputs/bundle/release/build-release.aab" + into getExportPath() + rename "build-release.aab", getExportFilename() +} diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 7d5d238100..e6c45b73a7 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,12 +1,13 @@ ext.versions = [ - androidGradlePlugin: '3.5.3', + androidGradlePlugin: '4.1.0', compileSdk : 29, minSdk : 18, targetSdk : 29, - buildTools : '29.0.3', + buildTools : '30.0.1', supportCoreUtils : '1.0.0', - kotlinVersion : '1.3.61', - v4Support : '1.0.0' + kotlinVersion : '1.4.10', + v4Support : '1.0.0', + javaVersion : 1.8 ] @@ -46,6 +47,37 @@ ext.getExportVersionName = { -> final String PLUGIN_VALUE_SEPARATOR_REGEX = "\\|" +// get the list of ABIs the project should be exported to +ext.getExportEnabledABIs = { -> + String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""; + if (enabledABIs == null || enabledABIs.isEmpty()) { + enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|" + } + Set<String> exportAbiFilter = []; + for (String abi_name : enabledABIs.split(PLUGIN_VALUE_SEPARATOR_REGEX)) { + if (!abi_name.trim().isEmpty()){ + exportAbiFilter.add(abi_name); + } + } + return exportAbiFilter; +} + +ext.getExportPath = { + String exportPath = project.hasProperty("export_path") ? project.property("export_path") : "" + if (exportPath == null || exportPath.isEmpty()) { + exportPath = "." + } + return exportPath +} + +ext.getExportFilename = { + String exportFilename = project.hasProperty("export_filename") ? project.property("export_filename") : "" + if (exportFilename == null || exportFilename.isEmpty()) { + exportFilename = "godot_android" + } + return exportFilename +} + /** * Parse the project properties for the 'plugins_maven_repos' property and return the list * of maven repos. diff --git a/platform/android/java/app/res/drawable/splash.png b/platform/android/java/app/res/drawable/splash.png Binary files differnew file mode 100644 index 0000000000..7bddd4325a --- /dev/null +++ b/platform/android/java/app/res/drawable/splash.png diff --git a/platform/android/java/app/res/drawable/splash_bg_color.png b/platform/android/java/app/res/drawable/splash_bg_color.png Binary files differnew file mode 100644 index 0000000000..004b6fd508 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_bg_color.png diff --git a/platform/android/java/app/res/drawable/splash_drawable.xml b/platform/android/java/app/res/drawable/splash_drawable.xml new file mode 100644 index 0000000000..2794a40817 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_drawable.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/splash_bg_color" /> + + <item> + <bitmap + android:gravity="center" + android:src="@drawable/splash" /> + </item> + +</layer-list> diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml new file mode 100644 index 0000000000..99f723f5ba --- /dev/null +++ b/platform/android/java/app/res/values/themes.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/> + + <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme"> + <item name="android:windowBackground">@drawable/splash_drawable</item> + <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> + </style> +</resources> diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 1af5950cbe..51df70969e 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -32,9 +32,16 @@ package com.godot.game; import org.godotengine.godot.FullScreenGodotApp; +import android.os.Bundle; + /** * Template activity for Godot Android custom builds. * Feel free to extend and modify this class for your custom logic. */ public class GodotApp extends FullScreenGodotApp { + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(R.style.GodotAppMainTheme); + super.onCreate(savedInstanceState); + } } diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties index e14cd5ba5c..2dc069ad2f 100644 --- a/platform/android/java/gradle.properties +++ b/platform/android/java/gradle.properties @@ -18,3 +18,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + +org.gradle.warning.mode=all diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index f56b0f6a5e..a7d8a0f310 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 19eee5a315..e3c5a02203 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -18,6 +18,11 @@ android { targetSdkVersion versions.targetSdk } + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + lintOptions { abortOnError false disable 'MissingTranslation', 'UnusedResources' @@ -50,15 +55,6 @@ android { def buildType = variant.buildType.name.capitalize() - def taskPrefix = "" - if (project.path != ":") { - taskPrefix = project.path + ":" - } - - // Disable the externalNativeBuild* task as it would cause build failures since the cmake build - // files is only setup for editing support. - gradle.startParameter.excludedTaskNames += taskPrefix + "externalNativeBuild" + buildType - def releaseTarget = buildType.toLowerCase() if (releaseTarget == null || releaseTarget == "") { throw new GradleException("Invalid build type: " + buildType) @@ -78,10 +74,4 @@ android { // Schedule the tasks so the generated libs are present before the aar file is packaged. tasks["merge${buildType}JniLibFolders"].dependsOn taskName } - - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } - } } diff --git a/platform/android/java/lib/res/values/dimens.xml b/platform/android/java/lib/res/values/dimens.xml new file mode 100644 index 0000000000..9034dbbcc1 --- /dev/null +++ b/platform/android/java/lib/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="text_edit_height">48dp</dimen> +</resources> diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index 138c2de94c..5aa48d87da 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -34,6 +34,8 @@ import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; /** @@ -43,13 +45,18 @@ import androidx.fragment.app.FragmentActivity; * within an Android app. */ public abstract class FullScreenGodotApp extends FragmentActivity { - protected Godot godotFragment; + @Nullable + private Godot godotFragment; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.godot_app_layout); - godotFragment = new Godot(); + godotFragment = initGodotInstance(); + if (godotFragment == null) { + throw new IllegalStateException("Godot instance must be non-null."); + } + getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } @@ -76,4 +83,17 @@ public abstract class FullScreenGodotApp extends FragmentActivity { } return super.onKeyMultiple(inKeyCode, repeatCount, event); } + + /** + * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}. + */ + @NonNull + protected Godot initGodotInstance() { + return new Godot(); + } + + @Nullable + protected final Godot getGodotFragment() { + return godotFragment; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 72746e06f7..6cf340c418 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -70,7 +70,7 @@ import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings.Secure; import android.view.Display; -import android.view.Gravity; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -83,7 +83,6 @@ import android.view.Window; import android.view.WindowManager; import android.widget.Button; import android.widget.FrameLayout; -import android.widget.PopupWindow; import android.widget.ProgressBar; import android.widget.TextView; @@ -153,8 +152,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void setButtonPausedState(boolean paused) { mStatePaused = paused; - int stringResourceID = paused ? R.string.text_button_resume : - R.string.text_button_pause; + int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; mPauseButton.setText(stringResourceID); } @@ -165,8 +163,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public GodotRenderView mRenderView; private boolean godot_initialized = false; - private PopupWindow mKeyboardWindow; - private SensorManager mSensorManager; private Sensor mAccelerometer; private Sensor mGravity; @@ -223,23 +219,12 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC containerLayout = new FrameLayout(activity); containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - // Create a popup window with an invisible layout for the virtual keyboard, - // so the view can be resized to get the vk height without resizing the main godot view. - final FrameLayout keyboardLayout = new FrameLayout(activity); - keyboardLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - keyboardLayout.setVisibility(View.INVISIBLE); - mKeyboardWindow = new PopupWindow(keyboardLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - mKeyboardWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - mKeyboardWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - mKeyboardWindow.setFocusable(true); // for the text edit to work - mKeyboardWindow.setTouchable(false); // inputs need to go through - // GodotEditText layout GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - editText.setKeyboardView(keyboardLayout); - // ...add to keyboard layout - keyboardLayout.addView(editText); + editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + (int)getResources().getDimension(R.dimen.text_edit_height))); + // ...add to FrameLayout + containerLayout.addView(editText); GodotLib.setup(command_line); @@ -256,13 +241,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC editText.setView(mRenderView); io.setEdit(editText); - keyboardLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Point fullSize = new Point(); activity.getWindowManager().getDefaultDisplay().getSize(fullSize); Rect gameSize = new Rect(); - mKeyboardWindow.getContentView().getWindowVisibleDisplayFrame(gameSize); + mRenderView.getView().getWindowVisibleDisplayFrame(gameSize); final int keyboardHeight = fullSize.y - gameSize.bottom; GodotLib.setVirtualKeyboardHeight(keyboardHeight); @@ -483,7 +468,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); @@ -620,21 +604,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } @Override - public void onStart() { - super.onStart(); - - mRenderView.getView().post(new Runnable() { - @Override - public void run() { - mKeyboardWindow.showAtLocation(getActivity().getWindow().getDecorView(), Gravity.NO_GRAVITY, 0, 0); - } - }); - } - - @Override public void onDestroy() { - mKeyboardWindow.dismiss(); - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onMainDestroy(); } @@ -885,63 +855,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } } - public boolean gotTouchEvent(final MotionEvent event) { - final int evcount = event.getPointerCount(); - if (evcount == 0) - return true; - - if (mRenderView != null) { - final int[] arr = new int[event.getPointerCount() * 3]; - - for (int i = 0; i < event.getPointerCount(); i++) { - arr[i * 3 + 0] = (int)event.getPointerId(i); - arr[i * 3 + 1] = (int)event.getX(i); - arr[i * 3 + 2] = (int)event.getY(i); - } - final int pointer_idx = event.getPointerId(event.getActionIndex()); - - //System.out.printf("gaction: %d\n",event.getAction()); - final int action = event.getAction() & MotionEvent.ACTION_MASK; - mRenderView.queueOnRenderThread(new Runnable() { - @Override - public void run() { - switch (action) { - case MotionEvent.ACTION_DOWN: { - GodotLib.touch(0, 0, evcount, arr); - //System.out.printf("action down at: %f,%f\n", event.getX(),event.getY()); - } break; - case MotionEvent.ACTION_MOVE: { - GodotLib.touch(1, 0, evcount, arr); - /* - for(int i=0;i<event.getPointerCount();i++) { - System.out.printf("%d - moved to: %f,%f\n",i, event.getX(i),event.getY(i)); - } - */ - } break; - case MotionEvent.ACTION_POINTER_UP: { - GodotLib.touch(4, pointer_idx, evcount, arr); - //System.out.printf("%d - s.up at: %f,%f\n",pointer_idx, event.getX(pointer_idx),event.getY(pointer_idx)); - } break; - case MotionEvent.ACTION_POINTER_DOWN: { - GodotLib.touch(3, pointer_idx, evcount, arr); - //System.out.printf("%d - s.down at: %f,%f\n",pointer_idx, event.getX(pointer_idx),event.getY(pointer_idx)); - } break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: { - GodotLib.touch(2, 0, evcount, arr); - /* - for(int i=0;i<event.getPointerCount();i++) { - System.out.printf("%d - up! %f,%f\n",i, event.getX(i),event.getY(i)); - } - */ - } break; - } - } - }); - } - return true; - } - public boolean onKeyMultiple(final int inKeyCode, int repeatCount, KeyEvent event) { String s = event.getCharacters(); if (s == null || s.length() == 0) diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index d169f46599..d731e080c4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,7 +29,6 @@ /*************************************************************************/ package org.godotengine.godot; - import org.godotengine.godot.input.GodotGestureHandler; import org.godotengine.godot.input.GodotInputHandler; import org.godotengine.godot.utils.GLUtils; @@ -127,7 +126,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); this.detector.onTouchEvent(event); - return godot.gotTouchEvent(event); + return inputHandler.onTouchEvent(event); } @Override 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 c2f3c88416..874fd88848 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -37,12 +37,16 @@ import android.content.*; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; +import android.graphics.Point; import android.media.*; import android.net.Uri; import android.os.*; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.WindowInsets; import java.io.IOException; import java.io.InputStream; @@ -461,6 +465,28 @@ public class GodotIO { return (int)(metrics.density * 160f); } + public int[] screenGetUsableRect() { + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + Display display = activity.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getRealSize(size); + + int result[] = { 0, 0, size.x, size.y }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets(); + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + int insetLeft = cutout.getSafeInsetLeft(); + int insetTop = cutout.getSafeInsetTop(); + result[0] = insetLeft; + result[1] = insetTop; + result[2] -= insetLeft + cutout.getSafeInsetRight(); + result[3] -= insetTop + cutout.getSafeInsetBottom(); + } + } + return result; + } + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (edit != null) edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); 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 318e2816ff..6ccbe91e60 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -94,17 +94,19 @@ public class GodotLib { /** * Forward touch events from the main thread to the GL thread. */ - public static native void touch(int what, int pointer, int howmany, int[] arr); + public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions); + public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask); + public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask, float verticalFactor, float horizontalFactor); /** * Forward hover events from the main thread to the GL thread. */ - public static native void hover(int type, int x, int y); + public static native void hover(int type, float x, float y); /** * Forward double_tap events from the main thread to the GL thread. */ - public static native void doubletap(int x, int y); + public static native void doubleTap(int buttonMask, int x, int y); /** * Forward scroll events from the main thread to the GL thread. diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 65708389c3..6cd5ca7b4e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -38,6 +38,7 @@ import org.godotengine.godot.vulkan.VkSurfaceView; import android.annotation.SuppressLint; import android.content.Context; import android.view.GestureDetector; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; @@ -100,22 +101,22 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); mGestureDetector.onTouchEvent(event); - return godot.gotTouchEvent(event); + return mInputHandler.onTouchEvent(event); } @Override public boolean onKeyUp(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); + return mInputHandler.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + return mInputHandler.onKeyDown(keyCode, event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { - return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); + return mInputHandler.onGenericMotionEvent(event); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index 042b3ac48a..c95339c583 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -39,7 +39,6 @@ import android.text.InputFilter; import android.text.InputType; import android.util.AttributeSet; import android.view.KeyEvent; -import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; @@ -57,7 +56,6 @@ public class GodotEditText extends EditText { // Fields // =========================================================== private GodotRenderView mRenderView; - private View mKeyboardView; private GodotTextInputWrapper mInputWrapper; private EditHandler sHandler = new EditHandler(this); private String mOriginText; @@ -131,7 +129,7 @@ public class GodotEditText extends EditText { edit.mInputWrapper.setOriginText(text); edit.addTextChangedListener(edit.mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mKeyboardView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(edit, 0); } } break; @@ -140,7 +138,7 @@ public class GodotEditText extends EditText { GodotEditText edit = (GodotEditText)msg.obj; edit.removeTextChangedListener(mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mKeyboardView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); edit.mRenderView.getView().requestFocus(); } break; @@ -164,10 +162,6 @@ public class GodotEditText extends EditText { view.getView().requestFocus(); } - public void setKeyboardView(final View keyboardView) { - mKeyboardView = keyboardView; - } - // =========================================================== // Methods for/from SuperClass/Interfaces // =========================================================== diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java index 1c9a683bbd..fb151fa504 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java @@ -33,7 +33,6 @@ package org.godotengine.godot.input; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; -import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; @@ -75,10 +74,11 @@ public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener //Log.i("GodotGesture", "onDoubleTap"); final int x = Math.round(event.getX()); final int y = Math.round(event.getY()); + final int buttonMask = event.getButtonState(); queueEvent(new Runnable() { @Override public void run() { - GodotLib.doubletap(x, y); + GodotLib.doubleTap(buttonMask, x, y); } }); return true; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 9abd65cc67..f3e985f944 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -36,6 +36,7 @@ import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; +import android.os.Build; import android.util.Log; import android.view.InputDevice; import android.view.InputDevice.MotionRange; @@ -156,6 +157,53 @@ public class GodotInputHandler implements InputDeviceListener { return true; } + public boolean onTouchEvent(final MotionEvent event) { + // Mouse drag (mouse pressed and move) doesn't fire onGenericMotionEvent so this is needed + if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (event.getAction() != MotionEvent.ACTION_MOVE) { + // we return true because every time a mouse event is fired, the event is already handled + // in onGenericMotionEvent, so by touch event we can say that the event is also handled + return true; + } + return handleMouseEvent(event); + } + + final int evcount = event.getPointerCount(); + if (evcount == 0) + return true; + + if (mRenderView != null) { + final float[] arr = new float[event.getPointerCount() * 3]; // pointerId1, x1, y1, pointerId2, etc... + + for (int i = 0; i < event.getPointerCount(); i++) { + arr[i * 3 + 0] = event.getPointerId(i); + arr[i * 3 + 1] = event.getX(i); + arr[i * 3 + 2] = event.getY(i); + } + final int action = event.getActionMasked(); + + mRenderView.queueOnRenderThread(new Runnable() { + @Override + public void run() { + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_MOVE: { + GodotLib.touch(event.getSource(), action, 0, evcount, arr); + } break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_POINTER_DOWN: { + int pointer_idx = event.getPointerId(event.getActionIndex()); + GodotLib.touch(event.getSource(), action, pointer_idx, evcount, arr); + } break; + } + } + }); + } + return true; + } + public boolean onGenericMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) { final int device_id = findJoystickDevice(event.getDeviceId()); @@ -189,8 +237,8 @@ public class GodotInputHandler implements InputDeviceListener { return true; } } else if ((event.getSource() & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS) { - final int x = Math.round(event.getX()); - final int y = Math.round(event.getY()); + final float x = event.getX(); + final float y = event.getY(); final int type = event.getAction(); queueEvent(new Runnable() { @Override @@ -199,6 +247,10 @@ public class GodotInputHandler implements InputDeviceListener { } }); return true; + } else if ((event.getSource() & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return handleMouseEvent(event); + } } return false; @@ -366,4 +418,53 @@ public class GodotInputHandler implements InputDeviceListener { return -1; } + + private boolean handleMouseEvent(final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: { + final float x = event.getX(); + final float y = event.getY(); + final int type = event.getAction(); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.hover(type, x, y); + } + }); + return true; + } + case MotionEvent.ACTION_BUTTON_PRESS: + case MotionEvent.ACTION_BUTTON_RELEASE: + case MotionEvent.ACTION_MOVE: { + final float x = event.getX(); + final float y = event.getY(); + final int buttonsMask = event.getButtonState(); + final int action = event.getAction(); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask); + } + }); + return true; + } + case MotionEvent.ACTION_SCROLL: { + final float x = event.getX(); + final float y = event.getY(); + final int buttonsMask = event.getButtonState(); + final int action = event.getAction(); + final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask, verticalFactor, horizontalFactor); + } + }); + } + } + return false; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt index aeb4628d5d..7fa8e3b4e5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -71,7 +71,7 @@ internal class VkRenderer { */ fun onVkSurfaceChanged(surface: Surface, width: Int, height: Int) { GodotLib.resize(surface, width, height) - + for (plugin in pluginRegistry.getAllPlugins()) { plugin.onVkSurfaceChanged(surface, width, height) } diff --git a/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml new file mode 100644 index 0000000000..dc180375d5 --- /dev/null +++ b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest package="org.godotengine.godot" /> diff --git a/platform/android/java/lib/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index d3bdf6a5f2..34925684da 100644 --- a/platform/android/java/lib/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -1,3 +1,4 @@ +# Non functional cmake build file used to provide Android Studio editor support to the project. cmake_minimum_required(VERSION 3.6) project(godot) diff --git a/platform/android/java/nativeSrcsConfigs/README.md b/platform/android/java/nativeSrcsConfigs/README.md new file mode 100644 index 0000000000..e48505ccda --- /dev/null +++ b/platform/android/java/nativeSrcsConfigs/README.md @@ -0,0 +1,4 @@ +## Native sources configs + +This is a non functional Android library used to provide Android Studio editor support to the Godot project native files. +Nothing else should be added to this library. diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle new file mode 100644 index 0000000000..65b7bb9dc9 --- /dev/null +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -0,0 +1,54 @@ +// Non functional android library used to provide Android Studio editor support to the project. +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + packagingOptions { + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + + // Should be uncommented for development purpose within Android Studio + // doNotStrip '**/*.so' + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + libraryVariants.all { variant -> + def buildType = variant.buildType.name.capitalize() + + def taskPrefix = "" + if (project.path != ":") { + taskPrefix = project.path + ":" + } + + // Disable the externalNativeBuild* task as it would cause build failures since the cmake build + // files is only setup for editing support. + gradle.startParameter.excludedTaskNames += taskPrefix + "externalNativeBuild" + buildType + } +} + +dependencies {} diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index f6921c70aa..524031d93f 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -3,3 +3,4 @@ rootProject.name = "Godot" include ':app' include ':lib' +include ':nativeSrcsConfigs' diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 4ccbc6b97e..8d1db395ab 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -29,7 +29,7 @@ /*************************************************************************/ #include "java_godot_io_wrapper.h" -#include "core/error_list.h" +#include "core/error/error_list.h" // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. @@ -52,6 +52,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); + _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); _hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V"); @@ -118,6 +119,19 @@ int GodotIOJavaWrapper::get_screen_dpi() { } } +void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { + if (_screen_get_usable_rect) { + JNIEnv *env = ThreadAndroid::get_env(); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _screen_get_usable_rect); + ERR_FAIL_COND(env->GetArrayLength(returnArray) != 4); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + for (int i = 0; i < 4; i++) { + p_rect_xywh[i] = arrayBody[i]; + } + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + } +} + String GodotIOJavaWrapper::get_unique_id() { if (_get_unique_id) { JNIEnv *env = ThreadAndroid::get_env(); diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 6465ded985..13270c794b 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -50,6 +50,7 @@ private: jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; + jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; jmethodID _show_keyboard = 0; jmethodID _hide_keyboard = 0; @@ -68,6 +69,7 @@ public: String get_locale(); String get_model(); int get_screen_dpi(); + void screen_get_usable_rect(int (&p_rect_xywh)[4]); String get_unique_id(); bool has_vk(); void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 4610b94363..5dc773fae2 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -37,9 +37,9 @@ #include "api/java_class_wrapper.h" #include "api/jni_singleton.h" #include "audio_driver_jandroid.h" -#include "core/engine.h" +#include "core/config/engine.h" +#include "core/config/project_settings.h" #include "core/input/input.h" -#include "core/project_settings.h" #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" @@ -51,6 +51,7 @@ #include "string_android.h" #include "thread_jandroid.h" +#include <android/input.h> #include <unistd.h> #include <android/native_window_jni.h> @@ -237,40 +238,51 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint count, jintArray positions) { +void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) { if (step == 0) return; Vector<DisplayServerAndroid::TouchPos> points; - for (int i = 0; i < count; i++) { - jint p[3]; - env->GetIntArrayRegion(positions, i * 3, 3, p); + for (int i = 0; i < pointer_count; i++) { + jfloat p[3]; + env->GetFloatArrayRegion(positions, i * 3, 3, p); DisplayServerAndroid::TouchPos tp; tp.pos = Point2(p[1], p[2]); - tp.id = p[0]; + tp.id = (int)p[0]; points.push_back(tp); } - DisplayServerAndroid::get_singleton()->process_touch(ev, pointer, points); + if ((input_device & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE) { + DisplayServerAndroid::get_singleton()->process_mouse_event(ev, buttons_mask, points[0].pos, vertical_factor, horizontal_factor); + } else { + DisplayServerAndroid::get_singleton()->process_touch(ev, pointer, points); + } +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position) { + touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position); +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FI(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position, jint buttons_mask) { + touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position, buttons_mask); +} - /* - if (os_android) - os_android->process_touch(ev,pointer,points); - */ +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FIFF(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) { + touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position, buttons_mask, vertical_factor, horizontal_factor); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jint p_x, jint p_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jfloat p_x, jfloat p_y) { if (step == 0) return; DisplayServerAndroid::get_singleton()->process_hover(p_type, Point2(p_x, p_y)); } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jclass clazz, jint p_x, jint p_y) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env, jclass clazz, jint p_button_mask, jint p_x, jint p_y) { if (step == 0) return; - DisplayServerAndroid::get_singleton()->process_double_tap(Point2(p_x, p_y)); + DisplayServerAndroid::get_singleton()->process_double_tap(p_button_mask, Point2(p_x, p_y)); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 07584518e5..b499f6dfa1 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -44,9 +44,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface, jboolean p_32_bits); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint count, jintArray positions); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jint p_x, jint p_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jclass clazz, jint p_x, jint p_y); +void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask = 0, jfloat vertical_factor = 0, jfloat horizontal_factor = 0); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FI(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FIFF(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jfloat p_x, jfloat p_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env, jclass clazz, jint p_button_mask, jint p_x, jint p_y); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); diff --git a/platform/android/jni_utils.h b/platform/android/jni_utils.h index 5320715853..bda056604f 100644 --- a/platform/android/jni_utils.h +++ b/platform/android/jni_utils.h @@ -32,8 +32,8 @@ #define JNI_UTILS_H #include "string_android.h" -#include <core/engine.h> -#include <core/variant.h> +#include <core/config/engine.h> +#include <core/variant/variant.h> #include <jni.h> struct jvalret { diff --git a/platform/android/logo.png b/platform/android/logo.png Binary files differindex df445f6a9c..f44d360a25 100644 --- a/platform/android/logo.png +++ b/platform/android/logo.png diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index baf6ee952a..00733f6dbb 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -30,8 +30,8 @@ #include "os_android.h" +#include "core/config/project_settings.h" #include "core/io/file_access_buffered_fa.h" -#include "core/project_settings.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "file_access_android.h" @@ -153,6 +153,7 @@ void OS_Android::main_loop_begin() { bool OS_Android::main_loop_iterate() { if (!main_loop) return false; + DisplayServerAndroid::get_singleton()->process_events(); return Main::iteration(); } diff --git a/platform/android/plugin/godot_plugin_config.h b/platform/android/plugin/godot_plugin_config.h index ea3c7b4f55..ecb9c0c7f5 100644 --- a/platform/android/plugin/godot_plugin_config.h +++ b/platform/android/plugin/godot_plugin_config.h @@ -31,9 +31,9 @@ #ifndef GODOT_PLUGIN_CONFIG_H #define GODOT_PLUGIN_CONFIG_H -#include "core/error_list.h" +#include "core/error/error_list.h" #include "core/io/config_file.h" -#include "core/ustring.h" +#include "core/string/ustring.h" static const char *PLUGIN_CONFIG_EXT = ".gdap"; diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index d2528bebeb..b8e5345b85 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -30,9 +30,9 @@ #include "godot_plugin_jni.h" -#include <core/engine.h> -#include <core/error_macros.h> -#include <core/project_settings.h> +#include <core/config/engine.h> +#include <core/config/project_settings.h> +#include <core/error/error_macros.h> #include <platform/android/api/jni_singleton.h> #include <platform/android/jni_utils.h> #include <platform/android/string_android.h> diff --git a/platform/android/string_android.h b/platform/android/string_android.h index 88ccd3b652..0a7dbf475d 100644 --- a/platform/android/string_android.h +++ b/platform/android/string_android.h @@ -30,7 +30,7 @@ #ifndef STRING_ANDROID_H #define STRING_ANDROID_H -#include "core/ustring.h" +#include "core/string/ustring.h" #include "thread_jandroid.h" #include <jni.h> diff --git a/platform/android/thread_jandroid.cpp b/platform/android/thread_jandroid.cpp index 13aa313ebf..442e4d781b 100644 --- a/platform/android/thread_jandroid.cpp +++ b/platform/android/thread_jandroid.cpp @@ -30,9 +30,9 @@ #include "thread_jandroid.h" +#include "core/object/script_language.h" #include "core/os/memory.h" -#include "core/safe_refcount.h" -#include "core/script_language.h" +#include "core/templates/safe_refcount.h" static void _thread_id_key_destr_callback(void *p_value) { memdelete(static_cast<Thread::ID *>(p_value)); diff --git a/platform/android/vulkan/vulkan_context_android.cpp b/platform/android/vulkan/vulkan_context_android.cpp index 5fb7a83da4..56ef99dfc7 100644 --- a/platform/android/vulkan/vulkan_context_android.cpp +++ b/platform/android/vulkan/vulkan_context_android.cpp @@ -29,6 +29,7 @@ /*************************************************************************/ #include "vulkan_context_android.h" + #include <vulkan/vulkan_android.h> const char *VulkanContextAndroid::_get_platform_surface_extension() const { diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 49c77468ed..1dd37dabe5 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -18,7 +18,9 @@ iphone_lib = [ "godot_view.mm", "display_layer.mm", "godot_view_renderer.mm", - "godot_view_gesture_recognizer.m", + "godot_view_gesture_recognizer.mm", + "device_metrics.m", + "native_video_view.m", ] env_ios = env.Clone() diff --git a/platform/iphone/app_delegate.mm b/platform/iphone/app_delegate.mm index 7edbcc4667..c9891309fe 100644 --- a/platform/iphone/app_delegate.mm +++ b/platform/iphone/app_delegate.mm @@ -29,7 +29,7 @@ /*************************************************************************/ #import "app_delegate.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #import "godot_view.h" #include "main/main.h" @@ -62,7 +62,7 @@ static ViewController *mainViewController = nil; CGRect windowBounds = [[UIScreen mainScreen] bounds]; // Create a full-screen window - self.window = [[[UIWindow alloc] initWithFrame:windowBounds] autorelease]; + self.window = [[UIWindow alloc] initWithFrame:windowBounds]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; @@ -140,7 +140,6 @@ static ViewController *mainViewController = nil; - (void)dealloc { self.window = nil; - [super dealloc]; } @end diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index f4ef40a0ba..ab453c353f 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -119,7 +119,7 @@ def configure(env): arch_flag = "i386" if env["arch"] == "x86" else env["arch"] env.Append( CCFLAGS=( - "-arch " + "-fobjc-arc -arch " + arch_flag + " -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks" " -fasm-blocks -isysroot $IPHONESDK -mios-simulator-version-min=13.0" @@ -129,7 +129,7 @@ def configure(env): detect_darwin_sdk_path("iphone", env) env.Append( CCFLAGS=( - "-fno-objc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing" + "-fobjc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing" " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" " -fpascal-strings -fblocks -isysroot $IPHONESDK -fvisibility=hidden -mthumb" ' "-DIBOutlet=__attribute__((iboutlet))"' @@ -141,7 +141,7 @@ def configure(env): detect_darwin_sdk_path("iphone", env) env.Append( CCFLAGS=( - "-fno-objc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing" + "-fobjc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing" " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" " -fpascal-strings -fblocks -fvisibility=hidden -MMD -MT dependencies -miphoneos-version-min=11.0" " -isysroot $IPHONESDK".split() @@ -235,7 +235,10 @@ def configure(env): env.Append(CPPDEFINES=["ICLOUD_ENABLED"]) env.Prepend( - CPPPATH=["$IPHONESDK/usr/include", "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers",] + CPPPATH=[ + "$IPHONESDK/usr/include", + "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers", + ] ) env["ENV"]["CODESIGN_ALLOCATE"] = "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate" diff --git a/platform/iphone/device_metrics.h b/platform/iphone/device_metrics.h new file mode 100644 index 0000000000..6d0ff49077 --- /dev/null +++ b/platform/iphone/device_metrics.h @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* device_metrics.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <Foundation/Foundation.h> + +@interface GodotDeviceMetrics : NSObject + +@property(nonatomic, class, readonly, strong) NSDictionary<NSArray *, NSNumber *> *dpiList; + +@end diff --git a/platform/iphone/device_metrics.m b/platform/iphone/device_metrics.m new file mode 100644 index 0000000000..747872bc49 --- /dev/null +++ b/platform/iphone/device_metrics.m @@ -0,0 +1,152 @@ +/*************************************************************************/ +/* device_metrics.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "device_metrics.h" + +@implementation GodotDeviceMetrics + ++ (NSDictionary *)dpiList { + return @{ + @[ + @"iPad1,1", + @"iPad2,1", + @"iPad2,2", + @"iPad2,3", + @"iPad2,4", + ] : @132, + @[ + @"iPhone1,1", + @"iPhone1,2", + @"iPhone2,1", + @"iPad2,5", + @"iPad2,6", + @"iPad2,7", + @"iPod1,1", + @"iPod2,1", + @"iPod3,1", + ] : @163, + @[ + @"iPad3,1", + @"iPad3,2", + @"iPad3,3", + @"iPad3,4", + @"iPad3,5", + @"iPad3,6", + @"iPad4,1", + @"iPad4,2", + @"iPad4,3", + @"iPad5,3", + @"iPad5,4", + @"iPad6,3", + @"iPad6,4", + @"iPad6,7", + @"iPad6,8", + @"iPad6,11", + @"iPad6,12", + @"iPad7,1", + @"iPad7,2", + @"iPad7,3", + @"iPad7,4", + @"iPad7,5", + @"iPad7,6", + @"iPad7,11", + @"iPad7,12", + @"iPad8,1", + @"iPad8,2", + @"iPad8,3", + @"iPad8,4", + @"iPad8,5", + @"iPad8,6", + @"iPad8,7", + @"iPad8,8", + @"iPad8,9", + @"iPad8,10", + @"iPad8,11", + @"iPad8,12", + @"iPad11,3", + @"iPad11,4", + ] : @264, + @[ + @"iPhone3,1", + @"iPhone3,2", + @"iPhone3,3", + @"iPhone4,1", + @"iPhone5,1", + @"iPhone5,2", + @"iPhone5,3", + @"iPhone5,4", + @"iPhone6,1", + @"iPhone6,2", + @"iPhone7,2", + @"iPhone8,1", + @"iPhone8,4", + @"iPhone9,1", + @"iPhone9,3", + @"iPhone10,1", + @"iPhone10,4", + @"iPhone11,8", + @"iPhone12,1", + @"iPhone12,8", + @"iPad4,4", + @"iPad4,5", + @"iPad4,6", + @"iPad4,7", + @"iPad4,8", + @"iPad4,9", + @"iPad5,1", + @"iPad5,2", + @"iPad11,1", + @"iPad11,2", + @"iPod4,1", + @"iPod5,1", + @"iPod7,1", + @"iPod9,1", + ] : @326, + @[ + @"iPhone7,1", + @"iPhone8,2", + @"iPhone9,2", + @"iPhone9,4", + @"iPhone10,2", + @"iPhone10,5", + ] : @401, + @[ + @"iPhone10,3", + @"iPhone10,6", + @"iPhone11,2", + @"iPhone11,4", + @"iPhone11,6", + @"iPhone12,3", + @"iPhone12,5", + ] : @458, + }; +} + +@end diff --git a/platform/iphone/display_layer.mm b/platform/iphone/display_layer.mm index 5ec94fb471..7420a5ebe6 100644 --- a/platform/iphone/display_layer.mm +++ b/platform/iphone/display_layer.mm @@ -29,8 +29,8 @@ /*************************************************************************/ #import "display_layer.h" +#include "core/config/project_settings.h" #include "core/os/keyboard.h" -#include "core/project_settings.h" #include "display_server_iphone.h" #include "main/main.h" #include "os_iphone.h" @@ -124,11 +124,8 @@ } if (context) { - [context release]; context = nil; } - - [super dealloc]; } - (BOOL)createFramebuffer { diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index aafee49594..6fa5e6ee4a 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -30,10 +30,12 @@ #include "display_server_iphone.h" #import "app_delegate.h" +#include "core/config/project_settings.h" #include "core/io/file_access_pack.h" -#include "core/project_settings.h" +#import "device_metrics.h" #import "godot_view.h" #include "ios.h" +#import "native_video_view.h" #include "os_iphone.h" #import "view_controller.h" @@ -41,120 +43,6 @@ #import <sys/utsname.h> static const float kDisplayServerIPhoneAcceleration = 1; -static NSDictionary *iOSModelToDPI = @{ - @[ - @"iPad1,1", - @"iPad2,1", - @"iPad2,2", - @"iPad2,3", - @"iPad2,4", - ] : @132, - @[ - @"iPhone1,1", - @"iPhone1,2", - @"iPhone2,1", - @"iPad2,5", - @"iPad2,6", - @"iPad2,7", - @"iPod1,1", - @"iPod2,1", - @"iPod3,1", - ] : @163, - @[ - @"iPad3,1", - @"iPad3,2", - @"iPad3,3", - @"iPad3,4", - @"iPad3,5", - @"iPad3,6", - @"iPad4,1", - @"iPad4,2", - @"iPad4,3", - @"iPad5,3", - @"iPad5,4", - @"iPad6,3", - @"iPad6,4", - @"iPad6,7", - @"iPad6,8", - @"iPad6,11", - @"iPad6,12", - @"iPad7,1", - @"iPad7,2", - @"iPad7,3", - @"iPad7,4", - @"iPad7,5", - @"iPad7,6", - @"iPad7,11", - @"iPad7,12", - @"iPad8,1", - @"iPad8,2", - @"iPad8,3", - @"iPad8,4", - @"iPad8,5", - @"iPad8,6", - @"iPad8,7", - @"iPad8,8", - @"iPad8,9", - @"iPad8,10", - @"iPad8,11", - @"iPad8,12", - @"iPad11,3", - @"iPad11,4", - ] : @264, - @[ - @"iPhone3,1", - @"iPhone3,2", - @"iPhone3,3", - @"iPhone4,1", - @"iPhone5,1", - @"iPhone5,2", - @"iPhone5,3", - @"iPhone5,4", - @"iPhone6,1", - @"iPhone6,2", - @"iPhone7,2", - @"iPhone8,1", - @"iPhone8,4", - @"iPhone9,1", - @"iPhone9,3", - @"iPhone10,1", - @"iPhone10,4", - @"iPhone11,8", - @"iPhone12,1", - @"iPhone12,8", - @"iPad4,4", - @"iPad4,5", - @"iPad4,6", - @"iPad4,7", - @"iPad4,8", - @"iPad4,9", - @"iPad5,1", - @"iPad5,2", - @"iPad11,1", - @"iPad11,2", - @"iPod4,1", - @"iPod5,1", - @"iPod7,1", - @"iPod9,1", - ] : @326, - @[ - @"iPhone7,1", - @"iPhone8,2", - @"iPhone9,2", - @"iPhone9,4", - @"iPhone10,2", - @"iPhone10,5", - ] : @401, - @[ - @"iPhone10,3", - @"iPhone10,6", - @"iPhone11,2", - @"iPhone11,4", - @"iPhone11,6", - @"iPhone12,3", - @"iPhone12,5", - ] : @458, -}; DisplayServerIPhone *DisplayServerIPhone::get_singleton() { return (DisplayServerIPhone *)DisplayServer::get_singleton(); @@ -383,8 +271,7 @@ void DisplayServerIPhone::update_gravity(float p_x, float p_y, float p_z) { Input::get_singleton()->set_gravity(Vector3(p_x, p_y, p_z)); }; -void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, - float p_z) { +void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, float p_z) { // Found out the Z should not be negated! Pass as is! Vector3 v_accelerometer = Vector3( p_x / kDisplayServerIPhoneAcceleration, @@ -392,39 +279,6 @@ void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, p_z / kDisplayServerIPhoneAcceleration); Input::get_singleton()->set_accelerometer(v_accelerometer); - - /* - if (p_x != last_accel.x) { - //printf("updating accel x %f\n", p_x); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_0; - ev.joy_motion.axis_value = (p_x / (float)ACCEL_RANGE); - last_accel.x = p_x; - queue_event(ev); - }; - if (p_y != last_accel.y) { - //printf("updating accel y %f\n", p_y); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_1; - ev.joy_motion.axis_value = (p_y / (float)ACCEL_RANGE); - last_accel.y = p_y; - queue_event(ev); - }; - if (p_z != last_accel.z) { - //printf("updating accel z %f\n", p_z); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_2; - ev.joy_motion.axis_value = ( (1.0 - p_z) / (float)ACCEL_RANGE); - last_accel.z = p_z; - queue_event(ev); - }; - */ }; void DisplayServerIPhone::update_magnetometer(float p_x, float p_y, float p_z) { @@ -492,7 +346,22 @@ Size2i DisplayServerIPhone::screen_get_size(int p_screen) const { } Rect2i DisplayServerIPhone::screen_get_usable_rect(int p_screen) const { - return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); + if (@available(iOS 11, *)) { + UIEdgeInsets insets = UIEdgeInsetsZero; + UIView *view = AppDelegate.viewController.godotView; + + if ([view respondsToSelector:@selector(safeAreaInsets)]) { + insets = [view safeAreaInsets]; + } + + float scale = screen_get_scale(p_screen); + Size2i insets_position = Size2i(insets.left, insets.top) * scale; + Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; + + return Rect2i(screen_get_position(p_screen) + insets_position, screen_get_size(p_screen) - insets_size); + } else { + return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); + } } int DisplayServerIPhone::screen_get_dpi(int p_screen) const { @@ -501,6 +370,8 @@ int DisplayServerIPhone::screen_get_dpi(int p_screen) const { NSString *string = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + NSDictionary *iOSModelToDPI = [GodotDeviceMetrics dpiList]; + for (NSArray *keyArray in iOSModelToDPI) { if ([keyArray containsObject:string]) { NSNumber *value = iOSModelToDPI[keyArray]; @@ -508,7 +379,26 @@ int DisplayServerIPhone::screen_get_dpi(int p_screen) const { } } - return 163; + // If device wasn't found in dictionary + // make a best guess from device metrics. + CGFloat scale = [UIScreen mainScreen].scale; + + UIUserInterfaceIdiom idiom = [UIDevice currentDevice].userInterfaceIdiom; + + switch (idiom) { + case UIUserInterfaceIdiomPad: + return scale == 2 ? 264 : 132; + case UIUserInterfaceIdiomPhone: { + if (scale == 3) { + CGFloat nativeScale = [UIScreen mainScreen].nativeScale; + return nativeScale == 3 ? 458 : 401; + } + + return 326; + } + default: + return 72; + } } float DisplayServerIPhone::screen_get_scale(int p_screen) const { @@ -686,7 +576,7 @@ Error DisplayServerIPhone::native_video_play(String p_path, float p_volume, Stri if (p_path.begins_with("res://")) { if (PackedData::get_singleton()->has_path(p_path)) { - printf("Unable to play %S using the native player as it resides in a .pck file\n", p_path.c_str()); + printf("Unable to play %s using the native player as it resides in a .pck file\n", p_path.utf8().get_data()); return ERR_INVALID_PARAMETER; } else { p_path = p_path.replace("res:/", ProjectSettings::get_singleton()->get_resource_path()); @@ -697,11 +587,11 @@ Error DisplayServerIPhone::native_video_play(String p_path, float p_volume, Stri memdelete(f); - printf("Playing video: %S\n", p_path.c_str()); + printf("Playing video: %s\n", p_path.utf8().get_data()); String file_path = ProjectSettings::get_singleton()->globalize_path(p_path); - NSString *filePath = [[[NSString alloc] initWithUTF8String:file_path.utf8().get_data()] autorelease]; + NSString *filePath = [[NSString alloc] initWithUTF8String:file_path.utf8().get_data()]; NSString *audioTrack = [NSString stringWithUTF8String:p_audio_track.utf8()]; NSString *subtitleTrack = [NSString stringWithUTF8String:p_subtitle_track.utf8()]; @@ -716,22 +606,22 @@ Error DisplayServerIPhone::native_video_play(String p_path, float p_volume, Stri } bool DisplayServerIPhone::native_video_is_playing() const { - return [AppDelegate.viewController isVideoPlaying]; + return [AppDelegate.viewController.videoView isVideoPlaying]; } void DisplayServerIPhone::native_video_pause() { if (native_video_is_playing()) { - [AppDelegate.viewController pauseVideo]; + [AppDelegate.viewController.videoView pauseVideo]; } } void DisplayServerIPhone::native_video_unpause() { - [AppDelegate.viewController unpauseVideo]; + [AppDelegate.viewController.videoView unpauseVideo]; }; void DisplayServerIPhone::native_video_stop() { if (native_video_is_playing()) { - [AppDelegate.viewController stopVideo]; + [AppDelegate.viewController.videoView stopVideo]; } } diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp index 4a751488cb..cf46883b92 100644 --- a/platform/iphone/export/export.cpp +++ b/platform/iphone/export/export.cpp @@ -30,13 +30,13 @@ #include "export.h" +#include "core/config/project_settings.h" #include "core/io/image_loader.h" #include "core/io/marshalls.h" #include "core/io/resource_saver.h" #include "core/io/zip_io.h" #include "core/os/file_access.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "core/version.h" #include "editor/editor_export.h" #include "editor/editor_node.h" @@ -86,6 +86,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { struct IOSExportAsset { String exported_path; bool is_framework; // framework is anything linked to the binary, otherwise it's a resource + bool should_embed; }; String _get_additional_plist_content(); @@ -100,7 +101,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<String> _get_preset_architectures(const Ref<EditorExportPreset> &p_preset); void _add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets); - Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets); + Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets); Error _export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets); bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { @@ -114,7 +115,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); @@ -159,9 +160,8 @@ public: void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name"); - if (driver == "GLES2") { - r_features->push_back("etc"); - } else if (driver == "Vulkan") { + r_features->push_back("pvrtc"); + if (driver == "Vulkan") { // FIXME: Review if this is correct. r_features->push_back("etc2"); } @@ -912,15 +912,6 @@ struct ExportLibsData { }; void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets) { - Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); - Vector<String> frameworks; - for (int i = 0; i < export_plugins.size(); ++i) { - Vector<String> plugin_frameworks = export_plugins[i]->get_ios_frameworks(); - for (int j = 0; j < plugin_frameworks.size(); ++j) { - frameworks.push_back(plugin_frameworks[j]); - } - } - // that is just a random number, we just need Godot IDs not to clash with // existing IDs in the project. PbxId current_id = { 0x58938401, 0, 0 }; @@ -945,15 +936,19 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese String type; if (asset.exported_path.ends_with(".framework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.framework"; } else if (asset.exported_path.ends_with(".xcframework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.xcframework"; } else if (asset.exported_path.ends_with(".dylib")) { @@ -1026,7 +1021,7 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } } -Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets) { +Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); String binary_name = p_out_dir.get_file().get_basename(); @@ -1035,7 +1030,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir String asset = p_assets[f_idx]; if (!asset.begins_with("res://")) { // either SDK-builtin or already a part of the export template - IOSExportAsset exported_asset = { asset, p_is_framework }; + IOSExportAsset exported_asset = { asset, p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); } else { DirAccess *da = DirAccess::create_for_path(asset); @@ -1101,7 +1096,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir memdelete(filesystem_da); return err; } - IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework }; + IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); if (create_framework) { @@ -1165,19 +1160,23 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets) { Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { - Vector<String> frameworks = export_plugins[i]->get_ios_frameworks(); - Error err = _export_additional_assets(p_out_dir, frameworks, true, r_exported_assets); + Vector<String> linked_frameworks = export_plugins[i]->get_ios_frameworks(); + Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets); + ERR_FAIL_COND_V(err, err); + + Vector<String> embedded_frameworks = export_plugins[i]->get_ios_embedded_frameworks(); + err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs(); for (int j = 0; j < project_static_libs.size(); j++) { project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project } - err = _export_additional_assets(p_out_dir, project_static_libs, true, r_exported_assets); + err = _export_additional_assets(p_out_dir, project_static_libs, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files(); - err = _export_additional_assets(p_out_dir, ios_bundle_files, false, r_exported_assets); + err = _export_additional_assets(p_out_dir, ios_bundle_files, false, false, r_exported_assets); ERR_FAIL_COND_V(err, err); } @@ -1185,7 +1184,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir for (int i = 0; i < p_libraries.size(); ++i) { library_paths.push_back(p_libraries[i].path); } - Error err = _export_additional_assets(p_out_dir, library_paths, true, r_exported_assets); + Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); return OK; @@ -1649,7 +1648,7 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset } } - String etc_error = test_etc2(); + String etc_error = test_etc2_or_pvrtc(); if (etc_error != String()) { valid = false; err += etc_error; diff --git a/platform/iphone/game_center.h b/platform/iphone/game_center.h index 1e9a68fe48..cbfd616e56 100644 --- a/platform/iphone/game_center.h +++ b/platform/iphone/game_center.h @@ -33,7 +33,7 @@ #ifndef GAME_CENTER_H #define GAME_CENTER_H -#include "core/object.h" +#include "core/object/class_db.h" class GameCenter : public Object { GDCLASS(GameCenter, Object); @@ -48,7 +48,7 @@ class GameCenter : public Object { void return_connect_error(const char *p_error_description); public: - void connect(); + Error authenticate(); bool is_authenticated(); Error post_score(Dictionary p_score); diff --git a/platform/iphone/game_center.mm b/platform/iphone/game_center.mm index 4481775c32..0f8c0100c3 100644 --- a/platform/iphone/game_center.mm +++ b/platform/iphone/game_center.mm @@ -52,6 +52,7 @@ extern "C" { GameCenter *GameCenter::instance = NULL; void GameCenter::_bind_methods() { + ClassDB::bind_method(D_METHOD("authenticate"), &GameCenter::authenticate); ClassDB::bind_method(D_METHOD("is_authenticated"), &GameCenter::is_authenticated); ClassDB::bind_method(D_METHOD("post_score"), &GameCenter::post_score); @@ -66,40 +67,28 @@ void GameCenter::_bind_methods() { ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); }; -void GameCenter::return_connect_error(const char *p_error_description) { - authenticated = false; - Dictionary ret; - ret["type"] = "authentication"; - ret["result"] = "error"; - ret["error_code"] = 0; - ret["error_description"] = p_error_description; - pending_events.push_back(ret); -} - -void GameCenter::connect() { +Error GameCenter::authenticate() { //if this class isn't available, game center isn't implemented if ((NSClassFromString(@"GKLocalPlayer")) == nil) { - return_connect_error("GameCenter not available"); - return; + return ERR_UNAVAILABLE; } GKLocalPlayer *player = [GKLocalPlayer localPlayer]; - if (![player respondsToSelector:@selector(authenticateHandler)]) { - return_connect_error("GameCenter doesn't respond to 'authenticateHandler'"); - return; - } + ERR_FAIL_COND_V(![player respondsToSelector:@selector(authenticateHandler)], ERR_UNAVAILABLE); ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; - if (!root_controller) { - return_connect_error("Window doesn't have root ViewController"); - return; - } + ERR_FAIL_COND_V(!root_controller, FAILED); // This handler is called several times. First when the view needs to be shown, then again // after the view is cancelled or the user logs in. Or if the user's already logged in, it's // called just once to confirm they're authenticated. This is why no result needs to be specified // in the presentViewController phase. In this case, more calls to this function will follow. + _weakify(root_controller); + _weakify(player); player.authenticateHandler = (^(UIViewController *controller, NSError *error) { + _strongify(root_controller); + _strongify(player); + if (controller) { [root_controller presentViewController:controller animated:YES completion:nil]; } else { @@ -126,6 +115,8 @@ void GameCenter::connect() { pending_events.push_back(ret); }; }); + + return OK; }; bool GameCenter::is_authenticated() { @@ -137,8 +128,8 @@ Error GameCenter::post_score(Dictionary p_score) { float score = p_score["score"]; String category = p_score["category"]; - NSString *cat_str = [[[NSString alloc] initWithUTF8String:category.utf8().get_data()] autorelease]; - GKScore *reporter = [[[GKScore alloc] initWithLeaderboardIdentifier:cat_str] autorelease]; + NSString *cat_str = [[NSString alloc] initWithUTF8String:category.utf8().get_data()]; + GKScore *reporter = [[GKScore alloc] initWithLeaderboardIdentifier:cat_str]; reporter.value = score; ERR_FAIL_COND_V([GKScore respondsToSelector:@selector(reportScores)], ERR_UNAVAILABLE); @@ -166,8 +157,8 @@ Error GameCenter::award_achievement(Dictionary p_params) { String name = p_params["name"]; float progress = p_params["progress"]; - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; - GKAchievement *achievement = [[[GKAchievement alloc] initWithIdentifier:name_str] autorelease]; + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; + GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:name_str]; ERR_FAIL_COND_V(!achievement, FAILED); ERR_FAIL_COND_V([GKAchievement respondsToSelector:@selector(reportAchievements)], ERR_UNAVAILABLE); @@ -311,7 +302,7 @@ Error GameCenter::show_game_center(Dictionary p_params) { } } - GKGameCenterViewController *controller = [[[GKGameCenterViewController alloc] init] autorelease]; + GKGameCenterViewController *controller = [[GKGameCenterViewController alloc] init]; ERR_FAIL_COND_V(!controller, FAILED); ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; @@ -323,7 +314,7 @@ Error GameCenter::show_game_center(Dictionary p_params) { controller.leaderboardIdentifier = nil; if (p_params.has("leaderboard_name")) { String name = p_params["leaderboard_name"]; - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; controller.leaderboardIdentifier = name_str; } } diff --git a/platform/iphone/godot_iphone.mm b/platform/iphone/godot_iphone.mm index 090b772947..a4119bcafd 100644 --- a/platform/iphone/godot_iphone.mm +++ b/platform/iphone/godot_iphone.mm @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "core/ustring.h" +#include "core/string/ustring.h" #include "main/main.h" #include "os_iphone.h" @@ -49,10 +49,8 @@ int add_path(int p_argc, char **p_args) { } p_args[p_argc++] = (char *)"--path"; - [str retain]; // memory leak lol (maybe make it static here and delete it in ViewController destructor? @todo p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding]; p_args[p_argc] = NULL; - [str release]; return p_argc; }; @@ -68,9 +66,7 @@ int add_cmdline(int p_argc, char **p_args) { if (!str) { continue; } - [str retain]; // @todo delete these at some point p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding]; - [str release]; }; p_args[p_argc] = NULL; diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm index c0a31549c4..eaf7e962ce 100644 --- a/platform/iphone/godot_view.mm +++ b/platform/iphone/godot_view.mm @@ -30,7 +30,7 @@ #import "godot_view.h" #include "core/os/keyboard.h" -#include "core/ustring.h" +#include "core/string/ustring.h" #import "display_layer.h" #include "display_server_iphone.h" #import "godot_view_gesture_recognizer.h" @@ -145,8 +145,6 @@ static const int max_touches = 8; if (self.delayGestureRecognizer) { self.delayGestureRecognizer = nil; } - - [super dealloc]; } - (void)godot_commonInit { @@ -156,7 +154,7 @@ static const int max_touches = 8; // Configure and start accelerometer if (!self.motionManager) { - self.motionManager = [[[CMMotionManager alloc] init] autorelease]; + self.motionManager = [[CMMotionManager alloc] init]; if (self.motionManager.deviceMotionAvailable) { self.motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXMagneticNorthZVertical]; @@ -169,7 +167,6 @@ static const int max_touches = 8; GodotViewGestureRecognizer *gestureRecognizer = [[GodotViewGestureRecognizer alloc] init]; self.delayGestureRecognizer = gestureRecognizer; [self addGestureRecognizer:self.delayGestureRecognizer]; - [gestureRecognizer release]; } - (void)stopRendering { @@ -204,14 +201,11 @@ static const int max_touches = 8; if (self.useCADisplayLink) { self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; - // if (@available(iOS 10, *)) { - self.displayLink.preferredFramesPerSecond = (NSInteger)(1.0 / self.renderingInterval); - // } else { - // // Approximate frame rate - // // assumes device refreshes at 60 fps - // int frameInterval = (int)floor(self.renderingInterval * 60.0f); - // [self.displayLink setFrameInterval:frameInterval]; - // } + // Approximate frame rate + // assumes device refreshes at 60 fps + int displayFPS = (NSInteger)(1.0 / self.renderingInterval); + + self.displayLink.preferredFramesPerSecond = displayFPS; // Setup DisplayLink in main thread [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; diff --git a/platform/iphone/godot_view_gesture_recognizer.h b/platform/iphone/godot_view_gesture_recognizer.h index ca3bd808d1..1431a9fb89 100644 --- a/platform/iphone/godot_view_gesture_recognizer.h +++ b/platform/iphone/godot_view_gesture_recognizer.h @@ -32,13 +32,15 @@ // emulating UIScrollView's UIScrollViewDelayedTouchesBeganGestureRecognizer. // It catches all gestures incoming to UIView and delays them for 150ms // (the same value used by UIScrollViewDelayedTouchesBeganGestureRecognizer) -// If touch cancelation or end message is fired it fires delayed +// If touch cancellation or end message is fired it fires delayed // begin touch immediately as well as last touch signal #import <UIKit/UIKit.h> @interface GodotViewGestureRecognizer : UIGestureRecognizer +@property(nonatomic, readonly, assign) NSTimeInterval delayTimeInterval; + - (instancetype)init; @end diff --git a/platform/iphone/godot_view_gesture_recognizer.m b/platform/iphone/godot_view_gesture_recognizer.mm index 377ccd52a5..91cc07fcf6 100644 --- a/platform/iphone/godot_view_gesture_recognizer.m +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -1,5 +1,5 @@ /*************************************************************************/ -/* godot_view_gesture_recognizer.m */ +/* godot_view_gesture_recognizer.mm */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -30,8 +30,7 @@ #import "godot_view_gesture_recognizer.h" -// Using same delay interval that is used for `UIScrollView` -const NSTimeInterval kGLGestureDelayInterval = 0.150; +#include "core/config/project_settings.h" // Minimum distance for touches to move to fire // a delay timer before scheduled time. @@ -41,6 +40,12 @@ const CGFloat kGLGestureMovementDistance = 0.5; @interface GodotViewGestureRecognizer () +@property(nonatomic, readwrite, assign) NSTimeInterval delayTimeInterval; + +@end + +@interface GodotViewGestureRecognizer () + // Timer used to delay begin touch message. // Should work as simple emulation of UIDelayedAction @property(strong, nonatomic) NSTimer *delayTimer; @@ -60,6 +65,8 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.delaysTouchesBegan = YES; self.delaysTouchesEnded = YES; + self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); + return self; } @@ -76,8 +83,6 @@ const CGFloat kGLGestureMovementDistance = 0.5; if (self.delayedEvent) { self.delayedEvent = nil; } - - [super dealloc]; } - (void)delayTouches:(NSSet *)touches andEvent:(UIEvent *)event { @@ -87,7 +92,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.delayedEvent = event; self.delayTimer = [NSTimer - scheduledTimerWithTimeInterval:kGLGestureDelayInterval + scheduledTimerWithTimeInterval:self.delayTimeInterval target:self selector:@selector(fireDelayedTouches:) userInfo:nil @@ -109,7 +114,6 @@ const CGFloat kGLGestureMovementDistance = 0.5; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseBegan]; [self delayTouches:cleared andEvent:event]; - [cleared release]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { @@ -130,17 +134,14 @@ const CGFloat kGLGestureMovementDistance = 0.5; if (distance > kGLGestureMovementDistance) { [self.delayTimer fire]; [self.view touchesMoved:cleared withEvent:event]; - [cleared release]; return; } } - [cleared release]; return; } [self.view touchesMoved:cleared withEvent:event]; - [cleared release]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { @@ -148,7 +149,6 @@ const CGFloat kGLGestureMovementDistance = 0.5; NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseEnded]; [self.view touchesEnded:cleared withEvent:event]; - [cleared release]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { diff --git a/platform/iphone/godot_view_renderer.mm b/platform/iphone/godot_view_renderer.mm index 1fc822b457..f2ff417e6f 100644 --- a/platform/iphone/godot_view_renderer.mm +++ b/platform/iphone/godot_view_renderer.mm @@ -29,8 +29,8 @@ /*************************************************************************/ #import "godot_view_renderer.h" +#include "core/config/project_settings.h" #include "core/os/keyboard.h" -#include "core/project_settings.h" #import "display_server_iphone.h" #include "main/main.h" #include "os_iphone.h" @@ -44,7 +44,6 @@ @interface GodotViewRenderer () -@property(assign, nonatomic) BOOL hasFinishedLocaleSetup; @property(assign, nonatomic) BOOL hasFinishedProjectDataSetup; @property(assign, nonatomic) BOOL hasStartedMain; @property(assign, nonatomic) BOOL hasFinishedSetup; @@ -58,9 +57,8 @@ return NO; } - if (!self.hasFinishedLocaleSetup) { - [self setupLocaleAndUUID]; - return YES; + if (!OS::get_singleton()) { + exit(0); } if (!self.hasFinishedProjectDataSetup) { @@ -79,33 +77,6 @@ return NO; } -- (void)setupLocaleAndUUID { - self.hasFinishedLocaleSetup = YES; - - if (!OS::get_singleton()) { - exit(0); - } - - NSString *locale_code = [[NSLocale currentLocale] localeIdentifier]; - OSIPhone::get_singleton()->set_locale(String::utf8([locale_code UTF8String])); - - NSString *uuid; - if ([[UIDevice currentDevice] respondsToSelector:@selector(identifierForVendor)]) { - uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; - } else { - // before iOS 6, so just generate an identifier and store it - uuid = [[NSUserDefaults standardUserDefaults] objectForKey:@"identiferForVendor"]; - if (!uuid) { - CFUUIDRef cfuuid = CFUUIDCreate(NULL); - uuid = [(NSString *)CFUUIDCreateString(NULL, cfuuid) autorelease]; - CFRelease(cfuuid); - [[NSUserDefaults standardUserDefaults] setObject:uuid forKey:@"identifierForVendor"]; - } - } - - OSIPhone::get_singleton()->set_unique_id(String::utf8([uuid UTF8String])); -} - - (void)setupProjectData { self.hasFinishedProjectDataSetup = YES; diff --git a/platform/iphone/icloud.h b/platform/iphone/icloud.h index 381edfa718..5f59abdfc0 100644 --- a/platform/iphone/icloud.h +++ b/platform/iphone/icloud.h @@ -33,7 +33,7 @@ #ifndef ICLOUD_H #define ICLOUD_H -#include "core/object.h" +#include "core/object/class_db.h" class ICloud : public Object { GDCLASS(ICloud, Object); diff --git a/platform/iphone/icloud.mm b/platform/iphone/icloud.mm index d3086e6cea..3d81349883 100644 --- a/platform/iphone/icloud.mm +++ b/platform/iphone/icloud.mm @@ -150,7 +150,7 @@ Variant nsobject_to_variant(NSObject *object) { NSObject *variant_to_nsobject(Variant v) { if (v.get_type() == Variant::STRING) { - return [[[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()] autorelease]; + return [[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()]; } else if (v.get_type() == Variant::FLOAT) { return [NSNumber numberWithDouble:(double)v]; } else if (v.get_type() == Variant::INT) { @@ -158,11 +158,11 @@ NSObject *variant_to_nsobject(Variant v) { } else if (v.get_type() == Variant::BOOL) { return [NSNumber numberWithBool:BOOL((bool)v)]; } else if (v.get_type() == Variant::DICTIONARY) { - NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease]; + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; Dictionary dic = v; Array keys = dic.keys(); for (int i = 0; i < keys.size(); ++i) { - NSString *key = [[[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()]; NSObject *value = variant_to_nsobject(dic[keys[i]]); if (key == NULL || value == NULL) { @@ -173,7 +173,7 @@ NSObject *variant_to_nsobject(Variant v) { } return result; } else if (v.get_type() == Variant::ARRAY) { - NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease]; + NSMutableArray *result = [[NSMutableArray alloc] init]; Array arr = v; for (int i = 0; i < arr.size(); ++i) { NSObject *value = variant_to_nsobject(arr[i]); @@ -195,7 +195,7 @@ NSObject *variant_to_nsobject(Variant v) { } Error ICloud::remove_key(String p_param) { - NSString *key = [[[NSString alloc] initWithUTF8String:p_param.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; @@ -217,7 +217,7 @@ Array ICloud::set_key_values(Dictionary p_params) { String variant_key = keys[i]; Variant variant_value = p_params[variant_key]; - NSString *key = [[[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()]; if (key == NULL) { error_keys.push_back(variant_key); continue; @@ -238,7 +238,7 @@ Array ICloud::set_key_values(Dictionary p_params) { } Variant ICloud::get_key_value(String p_param) { - NSString *key = [[[NSString alloc] initWithUTF8String:p_param.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; if (![[store dictionaryRepresentation] objectForKey:key]) { diff --git a/platform/iphone/in_app_store.h b/platform/iphone/in_app_store.h index beb58af2c7..85075e4e20 100644 --- a/platform/iphone/in_app_store.h +++ b/platform/iphone/in_app_store.h @@ -33,7 +33,18 @@ #ifndef IN_APP_STORE_H #define IN_APP_STORE_H -#include "core/object.h" +#include "core/object/class_db.h" + +#ifdef __OBJC__ +@class GodotProductsDelegate; +@class GodotTransactionsObserver; + +typedef GodotProductsDelegate InAppStoreProductDelegate; +typedef GodotTransactionsObserver InAppStoreTransactionObserver; +#else +typedef void InAppStoreProductDelegate; +typedef void InAppStoreTransactionObserver; +#endif class InAppStore : public Object { GDCLASS(InAppStore, Object); @@ -43,6 +54,9 @@ class InAppStore : public Object { List<Variant> pending_events; + InAppStoreProductDelegate *products_request_delegate; + InAppStoreTransactionObserver *transactions_observer; + public: Error request_product_info(Dictionary p_params); Error restore_purchases(); diff --git a/platform/iphone/in_app_store.mm b/platform/iphone/in_app_store.mm index dfec5d7634..3eec9dae69 100644 --- a/platform/iphone/in_app_store.mm +++ b/platform/iphone/in_app_store.mm @@ -32,59 +32,118 @@ #include "in_app_store.h" -extern "C" { #import <Foundation/Foundation.h> #import <StoreKit/StoreKit.h> -}; -bool auto_finish_transactions = true; -NSMutableDictionary *pending_transactions = [NSMutableDictionary dictionary]; -static NSArray *latestProducts; +InAppStore *InAppStore::instance = NULL; @interface SKProduct (LocalizedPrice) @property(nonatomic, readonly) NSString *localizedPrice; + @end //----------------------------------// // SKProduct extension //----------------------------------// @implementation SKProduct (LocalizedPrice) + - (NSString *)localizedPrice { NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:self.priceLocale]; NSString *formattedString = [numberFormatter stringFromNumber:self.price]; - [numberFormatter release]; return formattedString; } @end -InAppStore *InAppStore::instance = NULL; - -void InAppStore::_bind_methods() { - ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); - ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); - ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); +@interface GodotProductsDelegate : NSObject <SKProductsRequestDelegate> - ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); - ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); - ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); - ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); -}; +@property(nonatomic, strong) NSMutableArray *loadedProducts; +@property(nonatomic, strong) NSMutableArray *pendingRequests; -@interface ProductsDelegate : NSObject <SKProductsRequestDelegate> { -}; +- (void)performRequestWithProductIDs:(NSSet *)productIDs; +- (Error)purchaseProductWithProductID:(NSString *)productID; +- (void)reset; @end -@implementation ProductsDelegate +@implementation GodotProductsDelegate + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.loadedProducts = [NSMutableArray new]; + self.pendingRequests = [NSMutableArray new]; +} + +- (void)performRequestWithProductIDs:(NSSet *)productIDs { + SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs]; + + request.delegate = self; + [self.pendingRequests addObject:request]; + [request start]; +} + +- (Error)purchaseProductWithProductID:(NSString *)productID { + SKProduct *product = nil; + + NSLog(@"searching for product!"); + + if (self.loadedProducts) { + for (SKProduct *storedProduct in self.loadedProducts) { + if ([storedProduct.productIdentifier isEqualToString:productID]) { + product = storedProduct; + break; + } + } + } + + if (!product) { + return ERR_INVALID_PARAMETER; + } + + NSLog(@"product found!"); + + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + + NSLog(@"purchase sent!"); + + return OK; +} + +- (void)reset { + [self.loadedProducts removeAllObjects]; + [self.pendingRequests removeAllObjects]; +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + [self.pendingRequests removeObject:request]; + + Dictionary ret; + ret["type"] = "product_info"; + ret["result"] = "error"; + ret["error"] = String::utf8([error.localizedDescription UTF8String]); + + InAppStore::get_singleton()->_post_event(ret); +} - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { + [self.pendingRequests removeObject:request]; + NSArray *products = response.products; - latestProducts = products; + [self.loadedProducts addObjectsFromArray:products]; Dictionary ret; ret["type"] = "product_info"; @@ -108,7 +167,8 @@ void InAppStore::_bind_methods() { ids.push_back(String::utf8([product.productIdentifier UTF8String])); localized_prices.push_back(String::utf8([product.localizedPrice UTF8String])); currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String])); - }; + } + ret["titles"] = titles; ret["descriptions"] = descriptions; ret["prices"] = prices; @@ -120,52 +180,54 @@ void InAppStore::_bind_methods() { for (NSString *ipid in response.invalidProductIdentifiers) { invalid_ids.push_back(String::utf8([ipid UTF8String])); - }; + } + ret["invalid_ids"] = invalid_ids; InAppStore::get_singleton()->_post_event(ret); - - [request release]; -}; +} @end -Error InAppStore::request_product_info(Dictionary p_params) { - ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER); +@interface GodotTransactionsObserver : NSObject <SKPaymentTransactionObserver> - PackedStringArray pids = p_params["product_ids"]; - printf("************ request product info! %i\n", pids.size()); +@property(nonatomic, assign) BOOL shouldAutoFinishTransactions; +@property(nonatomic, strong) NSMutableDictionary *pendingTransactions; - NSMutableArray *array = [[[NSMutableArray alloc] initWithCapacity:pids.size()] autorelease]; - for (int i = 0; i < pids.size(); i++) { - printf("******** adding %ls to product list\n", pids[i].c_str()); - NSString *pid = [[[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()] autorelease]; - [array addObject:pid]; - }; +- (void)finishTransactionWithProductID:(NSString *)productID; +- (void)reset; - NSSet *products = [[[NSSet alloc] initWithArray:array] autorelease]; - SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:products]; +@end - ProductsDelegate *delegate = [[ProductsDelegate alloc] init]; +@implementation GodotTransactionsObserver - request.delegate = delegate; - [request start]; +- (instancetype)init { + self = [super init]; - return OK; -}; + if (self) { + [self godot_commonInit]; + } -Error InAppStore::restore_purchases() { - printf("restoring purchases!\n"); - [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + return self; +} - return OK; -}; +- (void)godot_commonInit { + self.pendingTransactions = [NSMutableDictionary new]; +} -@interface TransObserver : NSObject <SKPaymentTransactionObserver> { -}; -@end +- (void)finishTransactionWithProductID:(NSString *)productID { + SKPaymentTransaction *transaction = self.pendingTransactions[productID]; + + if (transaction) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } + + self.pendingTransactions[productID] = nil; +} -@implementation TransObserver +- (void)reset { + [self.pendingTransactions removeAllObjects]; +} - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { printf("transactions updated!\n"); @@ -183,32 +245,17 @@ Error InAppStore::restore_purchases() { ret["transaction_id"] = transactionId; NSData *receipt = nil; - int sdk_version = 6; - - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { - NSURL *receiptFileURL = nil; - NSBundle *bundle = [NSBundle mainBundle]; - if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) { - // Get the transaction receipt file path location in the app bundle. - receiptFileURL = [bundle appStoreReceiptURL]; - - // Read in the contents of the transaction file. - receipt = [NSData dataWithContentsOfURL:receiptFileURL]; - sdk_version = 7; - - } else { - // Fall back to deprecated transaction receipt, - // which is still available in iOS 7. + int sdk_version = [[[UIDevice currentDevice] systemVersion] intValue]; - // Use SKPaymentTransaction's transactionReceipt. - receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]; - } + NSBundle *bundle = [NSBundle mainBundle]; + // Get the transaction receipt file path location in the app bundle. + NSURL *receiptFileURL = [bundle appStoreReceiptURL]; - } else { - receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]; - } + // Read in the contents of the transaction file. + receipt = [NSData dataWithContentsOfURL:receiptFileURL]; NSString *receipt_to_send = nil; + if (receipt != nil) { receipt_to_send = [receipt base64EncodedStringWithOptions:0]; } @@ -219,13 +266,13 @@ Error InAppStore::restore_purchases() { InAppStore::get_singleton()->_post_event(ret); - if (auto_finish_transactions) { + if (self.shouldAutoFinishTransactions) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } else { - [pending_transactions setObject:transaction forKey:transaction.payment.productIdentifier]; + self.pendingTransactions[transaction.payment.productIdentifier] = transaction; } - }; break; + } break; case SKPaymentTransactionStateFailed: { printf("status transaction failed!\n"); String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); @@ -250,95 +297,119 @@ Error InAppStore::restore_purchases() { } break; default: { printf("status default %i!\n", (int)transaction.transactionState); - }; break; - }; - }; -}; + } break; + } + } +} @end -Error InAppStore::purchase(Dictionary p_params) { - ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); - if (![SKPaymentQueue canMakePayments]) - return ERR_UNAVAILABLE; +void InAppStore::_bind_methods() { + ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); + ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); + ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); - printf("purchasing!\n"); - ERR_FAIL_COND_V(!p_params.has("product_id"), ERR_INVALID_PARAMETER); + ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); + ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); + ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); + ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); +} - NSString *pid = [[[NSString alloc] initWithUTF8String:String(p_params["product_id"]).utf8().get_data()] autorelease]; +Error InAppStore::request_product_info(Dictionary p_params) { + ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER); - SKProduct *product = nil; + PackedStringArray pids = p_params["product_ids"]; + printf("************ request product info! %i\n", pids.size()); - if (latestProducts) { - for (SKProduct *storedProduct in latestProducts) { - if ([storedProduct.productIdentifier isEqualToString:pid]) { - product = storedProduct; - break; - } - } - } + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:pids.size()]; + for (int i = 0; i < pids.size(); i++) { + printf("******** adding %s to product list\n", pids[i].utf8().get_data()); + NSString *pid = [[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()]; + [array addObject:pid]; + }; - if (!product) { - return ERR_INVALID_PARAMETER; - } + NSSet *products = [[NSSet alloc] initWithArray:array]; - SKPayment *payment = [SKPayment paymentWithProduct:product]; - SKPaymentQueue *defq = [SKPaymentQueue defaultQueue]; - [defq addPayment:payment]; - printf("purchase sent!\n"); + [products_request_delegate performRequestWithProductIDs:products]; return OK; -}; +} + +Error InAppStore::restore_purchases() { + printf("restoring purchases!\n"); + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + + return OK; +} + +Error InAppStore::purchase(Dictionary p_params) { + ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); + if (![SKPaymentQueue canMakePayments]) { + return ERR_UNAVAILABLE; + } + + printf("purchasing!\n"); + Dictionary params = p_params; + ERR_FAIL_COND_V(!params.has("product_id"), ERR_INVALID_PARAMETER); + + NSString *pid = [[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()]; + + return [products_request_delegate purchaseProductWithProductID:pid]; +} int InAppStore::get_pending_event_count() { return pending_events.size(); -}; +} Variant InAppStore::pop_pending_event() { Variant front = pending_events.front()->get(); pending_events.pop_front(); return front; -}; +} void InAppStore::_post_event(Variant p_event) { pending_events.push_back(p_event); -}; +} void InAppStore::_record_purchase(String product_id) { String skey = "purchased/" + product_id; - NSString *key = [[[NSString alloc] initWithUTF8String:skey.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:skey.utf8().get_data()]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; -}; +} InAppStore *InAppStore::get_singleton() { return instance; -}; +} InAppStore::InAppStore() { ERR_FAIL_COND(instance != NULL); instance = this; - auto_finish_transactions = false; - TransObserver *observer = [[TransObserver alloc] init]; - [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; - //pending_transactions = [NSMutableDictionary dictionary]; -}; + products_request_delegate = [[GodotProductsDelegate alloc] init]; + transactions_observer = [[GodotTransactionsObserver alloc] init]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:transactions_observer]; +} void InAppStore::finish_transaction(String product_id) { NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding]; - if ([pending_transactions objectForKey:prod_id]) { - [[SKPaymentQueue defaultQueue] finishTransaction:[pending_transactions objectForKey:prod_id]]; - [pending_transactions removeObjectForKey:prod_id]; - } -}; + [transactions_observer finishTransactionWithProductID:prod_id]; +} void InAppStore::set_auto_finish_transaction(bool b) { - auto_finish_transactions = b; + transactions_observer.shouldAutoFinishTransactions = b; } -InAppStore::~InAppStore() {} +InAppStore::~InAppStore() { + [products_request_delegate reset]; + [transactions_observer reset]; + + products_request_delegate = nil; + [[SKPaymentQueue defaultQueue] removeTransactionObserver:transactions_observer]; + transactions_observer = nil; +} #endif diff --git a/platform/iphone/ios.h b/platform/iphone/ios.h index 2b29e6f268..5415c36cd9 100644 --- a/platform/iphone/ios.h +++ b/platform/iphone/ios.h @@ -31,7 +31,7 @@ #ifndef IOS_H #define IOS_H -#include "core/object.h" +#include "core/object/class_db.h" class iOS : public Object { GDCLASS(iOS, Object); diff --git a/platform/iphone/ios.mm b/platform/iphone/ios.mm index ad26d0ada3..d4e099063f 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -30,6 +30,7 @@ #include "ios.h" #import "app_delegate.h" +#import "view_controller.h" #import <UIKit/UIKit.h> #include <sys/sysctl.h> @@ -68,25 +69,11 @@ String iOS::get_model() const { } String iOS::get_rate_url(int p_app_id) const { - String templ = "itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=APP_ID"; - String templ_iOS7 = "itms-apps://itunes.apple.com/app/idAPP_ID"; - String templ_iOS8 = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=APP_ID&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software"; + String app_url_path = "itms-apps://itunes.apple.com/app/idAPP_ID"; - //ios7 before - String ret = templ; + String ret = app_url_path.replace("APP_ID", String::num(p_app_id)); - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 7.1) { - // iOS 7 needs a different templateReviewURL @see https://github.com/arashpayan/appirater/issues/131 - ret = templ_iOS7; - } else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { - // iOS 8 needs a different templateReviewURL also @see https://github.com/arashpayan/appirater/issues/182 - ret = templ_iOS8; - } - - // ios7 for everything? - ret = templ_iOS7.replace("APP_ID", String::num(p_app_id)); - - printf("returning rate url %ls\n", ret.c_str()); + printf("returning rate url %s\n", ret.utf8().get_data()); return ret; }; diff --git a/platform/iphone/joypad_iphone.mm b/platform/iphone/joypad_iphone.mm index 6088f1c25c..22d7dd4bd2 100644 --- a/platform/iphone/joypad_iphone.mm +++ b/platform/iphone/joypad_iphone.mm @@ -29,7 +29,7 @@ /*************************************************************************/ #import "joypad_iphone.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #include "main/main.h" @@ -131,8 +131,6 @@ void JoypadIPhone::start_processing() { - (void)dealloc { [self finishObserving]; - - [super dealloc]; } - (int)getJoyIdForController:(GCController *)controller { @@ -251,8 +249,13 @@ void JoypadIPhone::start_processing() { // The extended gamepad profile has all the input you could possibly find on // a gamepad but will only be active if your gamepad actually has all of // these... - controller.extendedGamepad.valueChangedHandler = ^( - GCExtendedGamepad *gamepad, GCControllerElement *element) { + _weakify(self); + _weakify(controller); + + controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + int joy_id = [self getJoyIdForController:controller]; if (element == gamepad.buttonA) { @@ -304,71 +307,33 @@ void JoypadIPhone::start_processing() { Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_RIGHT, jx); }; }; + } else if (controller.microGamepad != nil) { + // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad + _weakify(self); + _weakify(controller); + + controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + + int joy_id = [self getJoyIdForController:controller]; + + if (element == gamepad.buttonA) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, gamepad.dpad.right.isPressed); + }; + }; } - // else if (controller.gamepad != nil) { - // // gamepad is the standard profile with 4 buttons, shoulder buttons and a - // // D-pad - // controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, - // GCControllerElement *element) { - // int joy_id = [self getJoyIdForController:controller]; - // - // if (element == gamepad.buttonA) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - // gamepad.buttonA.isPressed); - // } else if (element == gamepad.buttonB) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, - // gamepad.buttonB.isPressed); - // } else if (element == gamepad.buttonX) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - // gamepad.buttonX.isPressed); - // } else if (element == gamepad.buttonY) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, - // gamepad.buttonY.isPressed); - // } else if (element == gamepad.leftShoulder) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, - // gamepad.leftShoulder.isPressed); - // } else if (element == gamepad.rightShoulder) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, - // gamepad.rightShoulder.isPressed); - // } else if (element == gamepad.dpad) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - // gamepad.dpad.up.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - // gamepad.dpad.down.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - // gamepad.dpad.left.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - // gamepad.dpad.right.isPressed); - // }; - // }; - //#ifdef ADD_MICRO_GAMEPAD // disabling this for now, only available on iOS 9+, - // // while we are setting that as the minimum, seems our - // // build environment doesn't like it - // } else if (controller.microGamepad != nil) { - // // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad - // controller.microGamepad.valueChangedHandler = - // ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - // int joy_id = [self getJoyIdForController:controller]; - // - // if (element == gamepad.buttonA) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - // gamepad.buttonA.isPressed); - // } else if (element == gamepad.buttonX) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - // gamepad.buttonX.isPressed); - // } else if (element == gamepad.dpad) { - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - // gamepad.dpad.up.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - // gamepad.dpad.down.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - // gamepad.dpad.left.isPressed); - // Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - // gamepad.dpad.right.isPressed); - // }; - // }; - //#endif - // }; ///@TODO need to add support for controller.motion which gives us access to /// the orientation of the device (if supported) diff --git a/platform/iphone/logo.png b/platform/iphone/logo.png Binary files differindex 405b6f93ca..966d8aa70a 100644 --- a/platform/iphone/logo.png +++ b/platform/iphone/logo.png diff --git a/platform/iphone/native_video_view.h b/platform/iphone/native_video_view.h new file mode 100644 index 0000000000..d8687b3538 --- /dev/null +++ b/platform/iphone/native_video_view.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* native_video_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import <UIKit/UIKit.h> + +@interface GodotNativeVideoView : UIView + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; +- (BOOL)isVideoPlaying; +- (void)pauseVideo; +- (void)unpauseVideo; +- (void)stopVideo; + +@end diff --git a/platform/iphone/native_video_view.m b/platform/iphone/native_video_view.m new file mode 100644 index 0000000000..a4e9f209f0 --- /dev/null +++ b/platform/iphone/native_video_view.m @@ -0,0 +1,260 @@ +/*************************************************************************/ +/* native_video_view.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#import "native_video_view.h" +#import <AVFoundation/AVFoundation.h> + +@interface GodotNativeVideoView () + +@property(strong, nonatomic) AVAsset *avAsset; +@property(strong, nonatomic) AVPlayerItem *avPlayerItem; +@property(strong, nonatomic) AVPlayer *avPlayer; +@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer; +@property(assign, nonatomic) CMTime videoCurrentTime; +@property(assign, nonatomic) BOOL isVideoCurrentlyPlaying; + +@end + +@implementation GodotNativeVideoView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.isVideoCurrentlyPlaying = NO; + self.videoCurrentTime = kCMTimeZero; + + [self observeVideoAudio]; +} + +- (void)observeVideoAudio { + printf("******** adding observer for sound routing changes\n"); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(audioRouteChangeListenerCallback:) + name:AVAudioSessionRouteChangeNotification + object:nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == self.avPlayerItem && [keyPath isEqualToString:@"status"]) { + [self handleVideoOrPlayerStatus]; + } + + if (object == self.avPlayer && [keyPath isEqualToString:@"rate"]) { + [self handleVideoPlayRate]; + } +} + +// MARK: Video Audio + +- (void)audioRouteChangeListenerCallback:(NSNotification *)notification { + printf("*********** route changed!\n"); + NSDictionary *interuptionDict = notification.userInfo; + + NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; + + switch (routeChangeReason) { + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: { + NSLog(@"AVAudioSessionRouteChangeReasonNewDeviceAvailable"); + NSLog(@"Headphone/Line plugged in"); + } break; + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: { + NSLog(@"AVAudioSessionRouteChangeReasonOldDeviceUnavailable"); + NSLog(@"Headphone/Line was pulled. Resuming video play...."); + if ([self isVideoPlaying]) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self.avPlayer play]; // NOTE: change this line according your current player implementation + NSLog(@"resumed play"); + }); + } + } break; + case AVAudioSessionRouteChangeReasonCategoryChange: { + // called at start - also when other audio wants to play + NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange"); + } break; + } +} + +// MARK: Native Video Player + +- (void)handleVideoOrPlayerStatus { + if (self.avPlayerItem.status == AVPlayerItemStatusFailed || self.avPlayer.status == AVPlayerStatusFailed) { + [self stopVideo]; + } + + if (self.avPlayer.status == AVPlayerStatusReadyToPlay && self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && CMTimeCompare(self.videoCurrentTime, kCMTimeZero) == 0) { + // NSLog(@"time: %@", self.video_current_time); + [self.avPlayer seekToTime:self.videoCurrentTime]; + self.videoCurrentTime = kCMTimeZero; + } +} + +- (void)handleVideoPlayRate { + NSLog(@"Player playback rate changed: %.5f", self.avPlayer.rate); + if ([self isVideoPlaying] && self.avPlayer.rate == 0.0 && !self.avPlayer.error) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self.avPlayer play]; // NOTE: change this line according your current player implementation + NSLog(@"resumed play"); + }); + + NSLog(@" . . . PAUSED (or just started)"); + } +} + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack { + self.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]]; + + self.avPlayerItem = [AVPlayerItem playerItemWithAsset:self.avAsset]; + [self.avPlayerItem addObserver:self forKeyPath:@"status" options:0 context:nil]; + + self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem]; + self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer]; + + [self.avPlayer addObserver:self forKeyPath:@"status" options:0 context:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(playerItemDidReachEnd:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:[self.avPlayer currentItem]]; + + [self.avPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0]; + + [self.avPlayerLayer setFrame:self.bounds]; + [self.layer addSublayer:self.avPlayerLayer]; + [self.avPlayer play]; + + AVMediaSelectionGroup *audioGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + + NSMutableArray *allAudioParams = [NSMutableArray array]; + for (id track in audioGroup.options) { + NSString *language = [[track locale] localeIdentifier]; + NSLog(@"subtitle lang: %@", language); + + if ([language isEqualToString:audioTrack]) { + AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters]; + [audioInputParams setVolume:videoVolume atTime:kCMTimeZero]; + [audioInputParams setTrackID:[track trackID]]; + [allAudioParams addObject:audioInputParams]; + + AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; + [audioMix setInputParameters:allAudioParams]; + + [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup]; + [self.avPlayer.currentItem setAudioMix:audioMix]; + + break; + } + } + + AVMediaSelectionGroup *subtitlesGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; + NSArray *useableTracks = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitlesGroup.options withoutMediaCharacteristics:[NSArray arrayWithObject:AVMediaCharacteristicContainsOnlyForcedSubtitles]]; + + for (id track in useableTracks) { + NSString *language = [[track locale] localeIdentifier]; + NSLog(@"subtitle lang: %@", language); + + if ([language isEqualToString:subtitleTrack]) { + [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup]; + break; + } + } + + self.isVideoCurrentlyPlaying = YES; + + return true; +} + +- (BOOL)isVideoPlaying { + if (self.avPlayer.error) { + printf("Error during playback\n"); + } + return (self.avPlayer.rate > 0 && !self.avPlayer.error); +} + +- (void)pauseVideo { + self.videoCurrentTime = self.avPlayer.currentTime; + [self.avPlayer pause]; + self.isVideoCurrentlyPlaying = NO; +} + +- (void)unpauseVideo { + [self.avPlayer play]; + self.isVideoCurrentlyPlaying = YES; +} + +- (void)playerItemDidReachEnd:(NSNotification *)notification { + [self stopVideo]; +} + +- (void)finishPlayingVideo { + [self.avPlayer pause]; + [self.avPlayerLayer removeFromSuperlayer]; + self.avPlayerLayer = nil; + + if (self.avPlayerItem) { + [self.avPlayerItem removeObserver:self forKeyPath:@"status"]; + self.avPlayerItem = nil; + } + + if (self.avPlayer) { + [self.avPlayer removeObserver:self forKeyPath:@"status"]; + self.avPlayer = nil; + } + + self.avAsset = nil; + + self.isVideoCurrentlyPlaying = NO; +} + +- (void)stopVideo { + [self finishPlayingVideo]; + + [self removeFromSuperview]; +} + +@end diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index f3bde46717..c6f95869ee 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -84,8 +84,6 @@ private: virtual void finalize() override; String user_data_dir; - String unique_id; - String locale_code; bool is_focused = false; @@ -118,10 +116,8 @@ public: void set_user_data_dir(String p_dir); virtual String get_user_data_dir() const override; - void set_locale(String p_locale); virtual String get_locale() const override; - void set_unique_id(String p_id); virtual String get_unique_id() const override; virtual void vibrate_handheld(int p_duration_ms = 500) override; diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index f0bbbd39ca..32e9dda92b 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -32,10 +32,10 @@ #include "os_iphone.h" #import "app_delegate.h" +#include "core/config/project_settings.h" #include "core/io/file_access_pack.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" -#include "core/project_settings.h" #include "display_server_iphone.h" #include "drivers/unix/syslog_logger.h" #import "godot_view.h" @@ -46,10 +46,6 @@ #import <UIKit/UIKit.h> #import <dlfcn.h> -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif - #if defined(VULKAN_ENABLED) #include "servers/rendering/rasterizer_rd/rasterizer_rd.h" #import <QuartzCore/CAMetalLayer.h> @@ -132,7 +128,6 @@ void OSIPhone::initialize_modules() { #ifdef GAME_CENTER_ENABLED game_center = memnew(GameCenter); Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); - game_center->connect(); #endif #ifdef STOREKIT_ENABLED @@ -276,19 +271,14 @@ String OSIPhone::get_model_name() const { Error OSIPhone::shell_open(String p_uri) { NSString *urlPath = [[NSString alloc] initWithUTF8String:p_uri.utf8().get_data()]; NSURL *url = [NSURL URLWithString:urlPath]; - [urlPath release]; if (![[UIApplication sharedApplication] canOpenURL:url]) { return ERR_CANT_OPEN; } - printf("opening url %ls\n", p_uri.c_str()); + printf("opening url %s\n", p_uri.utf8().get_data()); - // if (@available(iOS 10, *)) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; - // } else { - // [[UIApplication sharedApplication] openURL:url]; - // } return OK; }; @@ -297,7 +287,7 @@ void OSIPhone::set_user_data_dir(String p_dir) { DirAccess *da = DirAccess::open(p_dir); user_data_dir = da->get_current_dir(); - printf("setting data dir to %ls from %ls\n", user_data_dir.c_str(), p_dir.c_str()); + printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); memdelete(da); } @@ -305,20 +295,20 @@ String OSIPhone::get_user_data_dir() const { return user_data_dir; } -void OSIPhone::set_locale(String p_locale) { - locale_code = p_locale; -} - String OSIPhone::get_locale() const { - return locale_code; -} + NSString *preferedLanguage = [NSLocale preferredLanguages].firstObject; + + if (preferedLanguage) { + return String::utf8([preferedLanguage UTF8String]).replace("-", "_"); + } -void OSIPhone::set_unique_id(String p_id) { - unique_id = p_id; + NSString *localeIdentifier = [[NSLocale currentLocale] localeIdentifier]; + return String::utf8([localeIdentifier UTF8String]).replace("-", "_"); } String OSIPhone::get_unique_id() const { - return unique_id; + NSString *uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; + return String::utf8([uuid UTF8String]); } void OSIPhone::vibrate_handheld(int p_duration_ms) { diff --git a/platform/iphone/platform_config.h b/platform/iphone/platform_config.h index bc190ba956..ec39ad0ba4 100644 --- a/platform/iphone/platform_config.h +++ b/platform/iphone/platform_config.h @@ -30,8 +30,13 @@ #include <alloca.h> -#define GLES2_INCLUDE_H <ES2/gl.h> - #define PLATFORM_REFCOUNT #define PTHREAD_RENAME_SELF + +#define _weakify(var) __weak typeof(var) GDWeak_##var = var; +#define _strongify(var) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong typeof(var) var = GDWeak_##var; \ + _Pragma("clang diagnostic pop") diff --git a/platform/iphone/view_controller.h b/platform/iphone/view_controller.h index dffdc01d4a..b0b31ae377 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -32,17 +32,15 @@ #import <UIKit/UIKit.h> @class GodotView; +@class GodotNativeVideoView; @interface ViewController : UIViewController <GKGameCenterControllerDelegate> -- (GodotView *)godotView; +@property(nonatomic, readonly, strong) GodotView *godotView; +@property(nonatomic, readonly, strong) GodotNativeVideoView *videoView; // MARK: Native Video Player - (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; -- (BOOL)isVideoPlaying; -- (void)pauseVideo; -- (void)unpauseVideo; -- (void)stopVideo; @end diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 31597f7856..e11eff423b 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -29,10 +29,11 @@ /*************************************************************************/ #import "view_controller.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" #include "display_server_iphone.h" #import "godot_view.h" #import "godot_view_renderer.h" +#import "native_video_view.h" #include "os_iphone.h" #import <GameController/GameController.h> @@ -40,16 +41,7 @@ @interface ViewController () @property(strong, nonatomic) GodotViewRenderer *renderer; - -// TODO: separate view to handle video -// AVPlayer-related properties -@property(strong, nonatomic) AVAsset *avAsset; -@property(strong, nonatomic) AVPlayerItem *avPlayerItem; -@property(strong, nonatomic) AVPlayer *avPlayer; -@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer; -@property(assign, nonatomic) CMTime videoCurrentTime; -@property(assign, nonatomic) BOOL isVideoCurrentlyPlaying; -@property(assign, nonatomic) BOOL videoHasFoundError; +@property(strong, nonatomic) GodotNativeVideoView *videoView; @end @@ -67,9 +59,6 @@ self.view = view; view.renderer = self.renderer; - - [renderer release]; - [view release]; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { @@ -93,9 +82,7 @@ } - (void)godot_commonInit { - self.isVideoCurrentlyPlaying = NO; - self.videoCurrentTime = kCMTimeZero; - self.videoHasFoundError = false; + // Initialize view controller values. } - (void)didReceiveMemoryWarning { @@ -107,7 +94,6 @@ [super viewDidLoad]; [self observeKeyboard]; - [self observeAudio]; if (@available(iOS 11.0, *)) { [self setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; @@ -128,33 +114,13 @@ object:nil]; } -- (void)observeAudio { - printf("******** adding observer for sound routing changes\n"); - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(audioRouteChangeListenerCallback:) - name:AVAudioSessionRouteChangeNotification - object:nil]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (object == self.avPlayerItem && [keyPath isEqualToString:@"status"]) { - [self handleVideoOrPlayerStatus]; - } - - if (object == self.avPlayer && [keyPath isEqualToString:@"rate"]) { - [self handleVideoPlayRate]; - } -} - - (void)dealloc { - [self stopVideo]; + [self.videoView stopVideo]; + self.videoView = nil; self.renderer = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [super dealloc]; } // MARK: Orientation @@ -233,166 +199,19 @@ } } -// MARK: Audio - -- (void)audioRouteChangeListenerCallback:(NSNotification *)notification { - printf("*********** route changed!\n"); - NSDictionary *interuptionDict = notification.userInfo; - - NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; - - switch (routeChangeReason) { - case AVAudioSessionRouteChangeReasonNewDeviceAvailable: { - NSLog(@"AVAudioSessionRouteChangeReasonNewDeviceAvailable"); - NSLog(@"Headphone/Line plugged in"); - } break; - case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: { - NSLog(@"AVAudioSessionRouteChangeReasonOldDeviceUnavailable"); - NSLog(@"Headphone/Line was pulled. Resuming video play...."); - if ([self isVideoPlaying]) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self.avPlayer play]; // NOTE: change this line according your current player implementation - NSLog(@"resumed play"); - }); - } - } break; - case AVAudioSessionRouteChangeReasonCategoryChange: { - // called at start - also when other audio wants to play - NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange"); - } break; - } -} - // MARK: Native Video Player -- (void)handleVideoOrPlayerStatus { - if (self.avPlayerItem.status == AVPlayerItemStatusFailed || self.avPlayer.status == AVPlayerStatusFailed) { - [self stopVideo]; - self.videoHasFoundError = true; - } - - if (self.avPlayer.status == AVPlayerStatusReadyToPlay && self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && CMTimeCompare(self.videoCurrentTime, kCMTimeZero) == 0) { - // NSLog(@"time: %@", self.video_current_time); - [self.avPlayer seekToTime:self.videoCurrentTime]; - self.videoCurrentTime = kCMTimeZero; - } -} - -- (void)handleVideoPlayRate { - NSLog(@"Player playback rate changed: %.5f", self.avPlayer.rate); - if ([self isVideoPlaying] && self.avPlayer.rate == 0.0 && !self.avPlayer.error) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self.avPlayer play]; // NOTE: change this line according your current player implementation - NSLog(@"resumed play"); - }); - - NSLog(@" . . . PAUSED (or just started)"); - } -} - - (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack { - self.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]]; - - self.avPlayerItem = [AVPlayerItem playerItemWithAsset:self.avAsset]; - [self.avPlayerItem addObserver:self forKeyPath:@"status" options:0 context:nil]; - - self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem]; - self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer]; - - [self.avPlayer addObserver:self forKeyPath:@"status" options:0 context:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(playerItemDidReachEnd:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:[self.avPlayer currentItem]]; - - [self.avPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0]; - - [self.avPlayerLayer setFrame:self.view.bounds]; - [self.view.layer addSublayer:self.avPlayerLayer]; - [self.avPlayer play]; - - AVMediaSelectionGroup *audioGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - - NSMutableArray *allAudioParams = [NSMutableArray array]; - for (id track in audioGroup.options) { - NSString *language = [[track locale] localeIdentifier]; - NSLog(@"subtitle lang: %@", language); - - if ([language isEqualToString:audioTrack]) { - AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters]; - [audioInputParams setVolume:videoVolume atTime:kCMTimeZero]; - [audioInputParams setTrackID:[track trackID]]; - [allAudioParams addObject:audioInputParams]; - - AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; - [audioMix setInputParameters:allAudioParams]; - - [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup]; - [self.avPlayer.currentItem setAudioMix:audioMix]; - - break; - } - } - - AVMediaSelectionGroup *subtitlesGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - NSArray *useableTracks = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitlesGroup.options withoutMediaCharacteristics:[NSArray arrayWithObject:AVMediaCharacteristicContainsOnlyForcedSubtitles]]; - - for (id track in useableTracks) { - NSString *language = [[track locale] localeIdentifier]; - NSLog(@"subtitle lang: %@", language); - - if ([language isEqualToString:subtitleTrack]) { - [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup]; - break; - } - } - - self.isVideoCurrentlyPlaying = YES; - - return true; -} - -- (BOOL)isVideoPlaying { - if (self.avPlayer.error) { - printf("Error during playback\n"); - } - return (self.avPlayer.rate > 0 && !self.avPlayer.error); -} - -- (void)pauseVideo { - self.videoCurrentTime = self.avPlayer.currentTime; - [self.avPlayer pause]; - self.isVideoCurrentlyPlaying = NO; -} - -- (void)unpauseVideo { - [self.avPlayer play]; - self.isVideoCurrentlyPlaying = YES; -} - -- (void)playerItemDidReachEnd:(NSNotification *)notification { - [self stopVideo]; -} - -- (void)stopVideo { - [self.avPlayer pause]; - [self.avPlayerLayer removeFromSuperlayer]; - self.avPlayerLayer = nil; - - if (self.avPlayerItem) { - [self.avPlayerItem removeObserver:self forKeyPath:@"status"]; - self.avPlayerItem = nil; - } - - if (self.avPlayer) { - [self.avPlayer removeObserver:self forKeyPath:@"status"]; - self.avPlayer = nil; + // If we are showing some video already, reuse existing view for new video. + if (self.videoView) { + return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack]; + } else { + // Create autoresizing view for video playback. + GodotNativeVideoView *videoView = [[GodotNativeVideoView alloc] initWithFrame:self.view.bounds]; + videoView.autoresizingMask = UIViewAutoresizingFlexibleWidth & UIViewAutoresizingFlexibleHeight; + [self.view addSubview:videoView]; + return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack]; } - - self.avAsset = nil; - - self.isVideoCurrentlyPlaying = NO; } // MARK: Delegates diff --git a/platform/iphone/vulkan_context_iphone.mm b/platform/iphone/vulkan_context_iphone.mm index cb4dbe7f85..d62e826957 100644 --- a/platform/iphone/vulkan_context_iphone.mm +++ b/platform/iphone/vulkan_context_iphone.mm @@ -35,14 +35,12 @@ const char *VulkanContextIPhone::_get_platform_surface_extension() const { return VK_MVK_IOS_SURFACE_EXTENSION_NAME; } -Error VulkanContextIPhone::window_create(DisplayServer::WindowID p_window_id, - CALayer *p_metal_layer, int p_width, - int p_height) { +Error VulkanContextIPhone::window_create(DisplayServer::WindowID p_window_id, CALayer *p_metal_layer, int p_width, int p_height) { VkIOSSurfaceCreateInfoMVK createInfo; createInfo.sType = VK_STRUCTURE_TYPE_IOS_SURFACE_CREATE_INFO_MVK; createInfo.pNext = NULL; createInfo.flags = 0; - createInfo.pView = p_metal_layer; + createInfo.pView = (__bridge const void *)p_metal_layer; VkSurfaceKHR surface; VkResult err = diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index dcf9a46bf9..7381ea13b7 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -9,6 +9,7 @@ javascript_files = [ "javascript_eval.cpp", "javascript_main.cpp", "os_javascript.cpp", + "api/javascript_tools_editor_plugin.cpp", ] build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] @@ -19,6 +20,7 @@ build = env.add_program(build_targets, javascript_files) js_libraries = [ "native/http_request.js", + "native/library_godot_audio.js", ] for lib in js_libraries: env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) @@ -54,7 +56,7 @@ out_files = [ zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".html"), ] -html_file = "#misc/dist/html/full-size.html" +html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" in_files = [js_wrapped, build[1], html_file] if env["threads_enabled"]: in_files.append(build[2]) @@ -66,5 +68,5 @@ env.Zip( zip_files, ZIPROOT=zip_dir, ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}", - ZIPCOMSTR="Archving $SOURCES as $TARGET", + ZIPCOMSTR="Archiving $SOURCES as $TARGET", ) diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 9c73e5c4c4..6fd6c0ddf1 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -29,12 +29,14 @@ /*************************************************************************/ #include "api.h" -#include "core/engine.h" +#include "core/config/engine.h" #include "javascript_eval.h" +#include "javascript_tools_editor_plugin.h" static JavaScript *javascript_eval; void register_javascript_api() { + JavaScriptToolsEditorPlugin::initialize(); ClassDB::register_virtual_class<JavaScript>(); javascript_eval = memnew(JavaScript); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval)); diff --git a/platform/javascript/api/javascript_eval.h b/platform/javascript/api/javascript_eval.h index 29229de8e3..389983077e 100644 --- a/platform/javascript/api/javascript_eval.h +++ b/platform/javascript/api/javascript_eval.h @@ -31,7 +31,7 @@ #ifndef JAVASCRIPT_EVAL_H #define JAVASCRIPT_EVAL_H -#include "core/object.h" +#include "core/object/class_db.h" class JavaScript : public Object { private: diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp new file mode 100644 index 0000000000..d44e8139a1 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -0,0 +1,153 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "javascript_tools_editor_plugin.h" + +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/os/dir_access.h" +#include "core/os/file_access.h" +#include "editor/editor_node.h" + +#include <emscripten/emscripten.h> + +static void _javascript_editor_init_callback() { + EditorNode::get_singleton()->add_editor_plugin(memnew(JavaScriptToolsEditorPlugin(EditorNode::get_singleton()))); +} + +void JavaScriptToolsEditorPlugin::initialize() { + EditorNode::add_init_callback(_javascript_editor_init_callback); +} + +JavaScriptToolsEditorPlugin::JavaScriptToolsEditorPlugin(EditorNode *p_editor) { + Variant v; + add_tool_menu_item("Download Project Source", this, "_download_zip", v); +} + +void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { + if (!Engine::get_singleton() || !Engine::get_singleton()->is_editor_hint()) { + WARN_PRINT("Project download is only available in Editor mode"); + return; + } + String resource_path = ProjectSettings::get_singleton()->get_resource_path(); + + FileAccess *src_f; + zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zipFile zip = zipOpen2("/tmp/project.zip", APPEND_STATUS_CREATE, NULL, &io); + String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; + _zip_recursive(resource_path, base_path, zip); + zipClose(zip, NULL); + EM_ASM({ + const path = "/tmp/project.zip"; + const size = FS.stat(path)["size"]; + const buf = new Uint8Array(size); + const fd = FS.open(path, "r"); + FS.read(fd, buf, 0, size); + FS.close(fd); + FS.unlink(path); + const blob = new Blob([buf], { type: "application/zip" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "project.zip"; + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }); +} + +void JavaScriptToolsEditorPlugin::_bind_methods() { + ClassDB::bind_method("_download_zip", &JavaScriptToolsEditorPlugin::_download_zip); +} + +void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { + FileAccess *f = FileAccess::open(p_path, FileAccess::READ); + if (!f) { + WARN_PRINT("Unable to open file for zipping: " + p_path); + return; + } + Vector<uint8_t> data; + int len = f->get_len(); + data.resize(len); + f->get_buffer(data.ptrw(), len); + f->close(); + memdelete(f); + + String path = p_path.replace_first(p_base_path, ""); + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipWriteInFileInZip(p_zip, data.ptr(), data.size()); + zipCloseFileInZip(p_zip); +} + +void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { + DirAccess *dir = DirAccess::open(p_path); + if (!dir) { + WARN_PRINT("Unable to open dir for zipping: " + p_path); + return; + } + dir->list_dir_begin(); + String cur = dir->get_next(); + while (!cur.empty()) { + String cs = p_path.plus_file(cur); + if (cur == "." || cur == ".." || cur == ".import") { + // Skip + } else if (dir->current_is_dir()) { + String path = cs.replace_first(p_base_path, "") + "/"; + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipCloseFileInZip(p_zip); + _zip_recursive(cs, p_base_path, p_zip); + } else { + _zip_file(cs, p_base_path, p_zip); + } + cur = dir->get_next(); + } +} +#endif diff --git a/platform/javascript/api/javascript_tools_editor_plugin.h b/platform/javascript/api/javascript_tools_editor_plugin.h new file mode 100644 index 0000000000..cc09fa4cd3 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.h @@ -0,0 +1,62 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H +#define JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "core/io/zip_io.h" +#include "editor/editor_plugin.h" + +class JavaScriptToolsEditorPlugin : public EditorPlugin { + GDCLASS(JavaScriptToolsEditorPlugin, EditorPlugin); + +private: + void _zip_file(String p_path, String p_base_path, zipFile p_zip); + void _zip_recursive(String p_path, String p_base_path, zipFile p_zip); + +protected: + static void _bind_methods(); + + void _download_zip(Variant p_v); + +public: + static void initialize(); + + JavaScriptToolsEditorPlugin(EditorNode *p_editor); +}; +#else +class JavaScriptToolsEditorPlugin { +public: + static void initialize() {} +}; +#endif + +#endif // JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 9604914b2c..ab6ed54cc8 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -30,35 +30,58 @@ #include "audio_driver_javascript.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" +#include "godot_audio.h" #include <emscripten.h> AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; bool AudioDriverJavaScript::is_available() { - return EM_ASM_INT({ - if (!(window.AudioContext || window.webkitAudioContext)) { - return 0; - } - return 1; - }) != 0; + return godot_audio_is_available() != 0; } const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() { - AudioDriverJavaScript::singleton->mix_to_js(); +#ifndef NO_THREADS +void AudioDriverJavaScript::_audio_thread_func(void *p_data) { + AudioDriverJavaScript *obj = static_cast<AudioDriverJavaScript *>(p_data); + while (!obj->quit) { + obj->lock(); + if (!obj->needs_process) { + obj->unlock(); + OS::get_singleton()->delay_usec(1000); // Give the browser some slack. + continue; + } + obj->_js_driver_process(); + obj->needs_process = false; + obj->unlock(); + } +} +#endif + +extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_start() { +#ifndef NO_THREADS + AudioDriverJavaScript::singleton->lock(); +#else + AudioDriverJavaScript::singleton->_js_driver_process(); +#endif +} + +extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_end() { +#ifndef NO_THREADS + AudioDriverJavaScript::singleton->needs_process = true; + AudioDriverJavaScript::singleton->unlock(); +#endif } extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { AudioDriverJavaScript::singleton->process_capture(sample); } -void AudioDriverJavaScript::mix_to_js() { - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); +void AudioDriverJavaScript::_js_driver_process() { int sample_count = memarr_len(internal_buffer) / channel_count; int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); audio_server_process(sample_count, stream_buffer); @@ -73,37 +96,12 @@ void AudioDriverJavaScript::process_capture(float sample) { } Error AudioDriverJavaScript::init() { - int mix_rate = GLOBAL_GET("audio/mix_rate"); + mix_rate = GLOBAL_GET("audio/mix_rate"); int latency = GLOBAL_GET("audio/output_latency"); - /* clang-format off */ - _driver_id = EM_ASM_INT({ - const MIX_RATE = $0; - const LATENCY = $1 / 1000; - return Module.IDHandler.add({ - 'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}), - 'input': null, - 'stream': null, - 'script': null - }); - }, mix_rate, latency); - /* clang-format on */ - - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); - buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count); - /* clang-format off */ - buffer_length = EM_ASM_INT({ - var ref = Module.IDHandler.get($0); - const ctx = ref['context']; - const BUFFER_LENGTH = $1; - const CHANNEL_COUNT = $2; - - var script = ctx.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT); - script.connect(ctx.destination); - ref['script'] = script; - return script.bufferSize; - }, _driver_id, buffer_length, channel_count); - /* clang-format on */ + channel_count = godot_audio_init(mix_rate, latency); + buffer_length = closest_power_of_2(latency * mix_rate / 1000); + buffer_length = godot_audio_create_processor(buffer_length, channel_count); if (!buffer_length) { return FAILED; } @@ -114,134 +112,60 @@ Error AudioDriverJavaScript::init() { internal_buffer = memnew_arr(float, buffer_length *channel_count); } - return internal_buffer ? OK : ERR_OUT_OF_MEMORY; + if (!internal_buffer) { + return ERR_OUT_OF_MEMORY; + } + return OK; } void AudioDriverJavaScript::start() { - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - var INTERNAL_BUFFER_PTR = $1; - - var audioDriverMixFunction = cwrap('audio_driver_js_mix'); - var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); - ref['script'].onaudioprocess = function(audioProcessingEvent) { - audioDriverMixFunction(); - - var input = audioProcessingEvent.inputBuffer; - var output = audioProcessingEvent.outputBuffer; - var internalBuffer = HEAPF32.subarray( - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT, - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); - - for (var channel = 0; channel < output.numberOfChannels; channel++) { - var outputData = output.getChannelData(channel); - // Loop through samples. - for (var sample = 0; sample < outputData.length; sample++) { - outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; - } - } - - if (ref['input']) { - var inputDataL = input.getChannelData(0); - var inputDataR = input.getChannelData(1); - for (var i = 0; i < inputDataL.length; i++) { - audioDriverProcessCapture(inputDataL[i]); - audioDriverProcessCapture(inputDataR[i]); - } - } - }; - }, _driver_id, internal_buffer); - /* clang-format on */ +#ifndef NO_THREADS + thread = Thread::create(_audio_thread_func, this); +#endif + godot_audio_start(internal_buffer); } void AudioDriverJavaScript::resume() { - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - if (ref && ref['context'] && ref['context'].resume) - ref['context'].resume(); - }, _driver_id); - /* clang-format on */ + godot_audio_resume(); } float AudioDriverJavaScript::get_latency() { - /* clang-format off */ - return EM_ASM_DOUBLE({ - const ref = Module.IDHandler.get($0); - var latency = 0; - if (ref && ref['context']) { - const ctx = ref['context']; - if (ctx.baseLatency) { - latency += ctx.baseLatency; - } - if (ctx.outputLatency) { - latency += ctx.outputLatency; - } - } - return latency; - }, _driver_id); - /* clang-format on */ + return godot_audio_get_latency(); } int AudioDriverJavaScript::get_mix_rate() const { - /* clang-format off */ - return EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].sampleRate : 0; - }, _driver_id); - /* clang-format on */ + return mix_rate; } AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { - /* clang-format off */ - return get_speaker_mode_by_total_channels(EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].destination.channelCount : 0; - }, _driver_id)); - /* clang-format on */ + return get_speaker_mode_by_total_channels(channel_count); } -// No locking, as threads are not supported. void AudioDriverJavaScript::lock() { +#ifndef NO_THREADS + mutex.lock(); +#endif } void AudioDriverJavaScript::unlock() { +#ifndef NO_THREADS + mutex.unlock(); +#endif } void AudioDriverJavaScript::finish_async() { - // Close the context, add the operation to the async_finish list in module. - int id = _driver_id; - _driver_id = 0; - - /* clang-format off */ - EM_ASM({ - const id = $0; - var ref = Module.IDHandler.get(id); - Module.async_finish.push(new Promise(function(accept, reject) { - if (!ref) { - console.log("Ref not found!", id, Module.IDHandler); - setTimeout(accept, 0); - } else { - Module.IDHandler.remove(id); - const context = ref['context']; - // Disconnect script and input. - ref['script'].disconnect(); - if (ref['input']) - ref['input'].disconnect(); - ref = null; - context.close().then(function() { - accept(); - }).catch(function(e) { - accept(); - }); - } - })); - }, id); - /* clang-format on */ +#ifndef NO_THREADS + quit = true; // Ask thread to quit. +#endif + godot_audio_finish_async(); } void AudioDriverJavaScript::finish() { +#ifndef NO_THREADS + Thread::wait_to_finish(thread); + memdelete(thread); + thread = NULL; +#endif if (internal_buffer) { memdelete_arr(internal_buffer); internal_buffer = nullptr; @@ -249,56 +173,14 @@ void AudioDriverJavaScript::finish() { } Error AudioDriverJavaScript::capture_start() { + godot_audio_capture_stop(); input_buffer_init(buffer_length); - - /* clang-format off */ - EM_ASM({ - function gotMediaInput(stream) { - var ref = Module.IDHandler.get($0); - ref['stream'] = stream; - ref['input'] = ref['context'].createMediaStreamSource(stream); - ref['input'].connect(ref['script']); - } - - function gotMediaInputError(e) { - out(e); - } - - if (navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); - } else { - if (!navigator.getUserMedia) - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); - } - }, _driver_id); - /* clang-format on */ - + godot_audio_capture_start(); return OK; } Error AudioDriverJavaScript::capture_stop() { - /* clang-format off */ - EM_ASM({ - var ref = Module.IDHandler.get($0); - if (ref['stream']) { - const tracks = ref['stream'].getTracks(); - for (var i = 0; i < tracks.length; i++) { - tracks[i].stop(); - } - ref['stream'] = null; - } - - if (ref['input']) { - ref['input'].disconnect(); - ref['input'] = null; - } - - }, _driver_id); - /* clang-format on */ - input_buffer.clear(); - return OK; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index f029a91db0..56a7da0307 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -33,34 +33,49 @@ #include "servers/audio_server.h" +#include "core/os/mutex.h" +#include "core/os/thread.h" + class AudioDriverJavaScript : public AudioDriver { +private: float *internal_buffer = nullptr; - int _driver_id = 0; int buffer_length = 0; + int mix_rate = 0; + int channel_count = 0; public: +#ifndef NO_THREADS + Mutex mutex; + Thread *thread = nullptr; + bool quit = false; + bool needs_process = true; + + static void _audio_thread_func(void *p_data); +#endif + + void _js_driver_process(); + static bool is_available(); - void mix_to_js(); void process_capture(float sample); static AudioDriverJavaScript *singleton; - virtual const char *get_name() const; + const char *get_name() const override; - virtual Error init(); - virtual void start(); + Error init() override; + void start() override; void resume(); - virtual float get_latency(); - virtual int get_mix_rate() const; - virtual SpeakerMode get_speaker_mode() const; - virtual void lock(); - virtual void unlock(); - virtual void finish(); + float get_latency() override; + int get_mix_rate() const override; + SpeakerMode get_speaker_mode() const override; + void lock() override; + void unlock() override; + void finish() override; void finish_async(); - virtual Error capture_start(); - virtual Error capture_stop(); + Error capture_start() override; + Error capture_stop() override; AudioDriverJavaScript(); }; diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 81287cead8..8f2961b33d 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,7 @@ import os -from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file +from emscripten_helpers import run_closure_compiler, create_engine_file +from SCons.Util import WhereIs def is_active(): @@ -12,7 +13,7 @@ def get_name(): def can_build(): - return "EM_CONFIG" in os.environ or os.path.exists(os.path.expanduser("~/.emscripten")) + return WhereIs("emcc") is not None def get_opts(): @@ -100,9 +101,6 @@ def configure(env): # Closure compiler extern and support for ecmascript specs (const, let, etc). env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6" - em_config = parse_config() - env.PrependENVPath("PATH", em_config["EMCC_ROOT"]) - env["CC"] = "emcc" env["CXX"] = "em++" env["LINK"] = "emcc" @@ -137,8 +135,9 @@ def configure(env): env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"]) env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"]) env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"]) - env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=4"]) + env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"]) + env.extra_suffix = ".threads" + env.extra_suffix else: env.Append(CPPDEFINES=["NO_THREADS"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 2f0a2faa83..8dc33bdf64 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -75,7 +75,7 @@ bool DisplayServerJavaScript::check_size_force_redraw() { if (last_width != canvas_width || last_height != canvas_height) { last_width = canvas_width; last_height = canvas_height; - // Update the framebuffer size and for redraw. + // Update the framebuffer size for redraw. emscripten_set_canvas_element_size(DisplayServerJavaScript::canvas_id, canvas_width, canvas_height); return true; } @@ -829,6 +829,19 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr } DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + r_error = OK; // Always succeeds for now. + + /* clang-format off */ + swap_cancel_ok = EM_ASM_INT({ + const win = (['Windows', 'Win64', 'Win32', 'WinCE']); + const plat = navigator.platform || ""; + if (win.indexOf(plat) !== -1) { + return 1; + } + return 0; + }) == 1; + /* clang-format on */ + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? #if 0 EmscriptenWebGLContextAttributes attributes; @@ -879,15 +892,18 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive #define SET_EM_CALLBACK(target, ev, cb) \ result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ EM_CHECK(ev) +#define SET_EM_WINDOW_CALLBACK(ev, cb) \ + result = emscripten_set_##ev##_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &cb); \ + EM_CHECK(ev) #define SET_EM_CALLBACK_NOTARGET(ev, cb) \ result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ EM_CHECK(ev) // These callbacks from Emscripten's html5.h suffice to access most // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM // is used below. - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback) SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) + SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) + SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) @@ -905,27 +921,25 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive /* clang-format off */ EM_ASM_ARGS({ - Module.listeners = {}; + // Bind native event listeners. + // Module.listeners, and Module.drop_handler are defined in native/utils.js const canvas = Module['canvas']; const send_window_event = cwrap('send_window_event', null, ['number']); const notifications = arguments; (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.listeners[event] = send_window_event.bind(null, notifications[index]); - canvas.addEventListener(event, Module.listeners[event]); + Module.listeners.add(canvas, event, send_window_event.bind(null, notifications[index]), true); }); // Clipboard const update_clipboard = cwrap('update_clipboard', null, ['string']); - Module.listeners['paste'] = function(evt) { + Module.listeners.add(window, 'paste', function(evt) { update_clipboard(evt.clipboardData.getData('text')); - }; - window.addEventListener('paste', Module.listeners['paste'], false); - Module.listeners['dragover'] = function(ev) { + }, false); + // Drag an drop + Module.listeners.add(canvas, 'dragover', function(ev) { // Prevent default behavior (which would try to open the file(s)) ev.preventDefault(); - }; - Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js - canvas.addEventListener('dragover', Module.listeners['dragover'], false); - canvas.addEventListener('drop', Module.listeners['drop'], false); + }, false); + Module.listeners.add(canvas, 'drop', Module.drop_handler, false); }, WINDOW_EVENT_MOUSE_ENTER, WINDOW_EVENT_MOUSE_EXIT, @@ -939,14 +953,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive DisplayServerJavaScript::~DisplayServerJavaScript() { EM_ASM({ - Object.entries(Module.listeners).forEach(function(kv) { - if (kv[0] == 'paste') { - window.removeEventListener(kv[0], kv[1], true); - } else { - Module['canvas'].removeEventListener(kv[0], kv[1]); - } - }); - Module.listeners = {}; + Module.listeners.clear(); }); //emscripten_webgl_commit_frame(); //emscripten_webgl_destroy_context(webgl_ctx); @@ -1096,7 +1103,11 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { last_width = p_size.x; last_height = p_size.y; - emscripten_set_canvas_element_size(canvas_id, p_size.x, p_size.y); + double scale = EM_ASM_DOUBLE({ + return window.devicePixelRatio || 1; + }); + emscripten_set_canvas_element_size(canvas_id, p_size.x * scale, p_size.y * scale); + emscripten_set_element_css_size(canvas_id, p_size.x, p_size.y); } Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const { @@ -1181,6 +1192,10 @@ int DisplayServerJavaScript::get_current_video_driver() const { return 1; } +bool DisplayServerJavaScript::get_swap_cancel_ok() { + return swap_cancel_ok; +} + void DisplayServerJavaScript::swap_buffers() { //emscripten_webgl_commit_frame(); } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index b149665d67..d7116be36f 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -56,6 +56,8 @@ class DisplayServerJavaScript : public DisplayServer { int last_width = 0; int last_height = 0; + bool swap_cancel_ok = false; + // utilities static Point2 compute_position_in_canvas(int p_x, int p_y); static void focus_canvas(); @@ -91,7 +93,7 @@ class DisplayServerJavaScript : public DisplayServer { static void _dispatch_input_event(const Ref<InputEvent> &p_event); protected: - virtual int get_current_video_driver() const; + int get_current_video_driver() const; public: // Override return type to make writing static callbacks less tedious. @@ -111,91 +113,92 @@ public: bool check_size_force_redraw(); // from DisplayServer - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - virtual bool has_feature(Feature p_feature) const; - virtual String get_name() const; + void alert(const String &p_alert, const String &p_title = "ALERT!") override; + bool has_feature(Feature p_feature) const override; + String get_name() const override; // cursor - virtual void cursor_set_shape(CursorShape p_shape); - virtual CursorShape cursor_get_shape() const; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()); + void cursor_set_shape(CursorShape p_shape) override; + CursorShape cursor_get_shape() const override; + void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; // mouse - virtual void mouse_set_mode(MouseMode p_mode); - virtual MouseMode mouse_get_mode() const; + void mouse_set_mode(MouseMode p_mode) override; + MouseMode mouse_get_mode() const override; // touch - virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; // clipboard - virtual void clipboard_set(const String &p_text); - virtual String clipboard_get() const; + void clipboard_set(const String &p_text) override; + String clipboard_get() const override; // screen - virtual int get_screen_count() const; - virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + int get_screen_count() const override; + Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; // windows - virtual Vector<DisplayServer::WindowID> get_window_list() const; - virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; + Vector<DisplayServer::WindowID> get_window_list() const override; + WindowID get_window_at_screen_position(const Point2i &p_position) const override; - virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID); - virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const; + void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; + ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; - virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); - virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); - virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; - virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; - virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; - virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const; - virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID); + int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; + void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override; - virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const; - virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID); + Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override; + void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override; - virtual void window_set_transient(WindowID p_window, WindowID p_parent); + void window_set_transient(WindowID p_window, WindowID p_parent) override; - virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); - virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); - virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); - virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const; - virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const; // FIXME: Find clearer name for this. + void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override; + Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID); - virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override; + WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const; + bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID); - virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override; + bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID); - virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID); + void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; + void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; - virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const; + bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual bool can_any_window_draw() const; + bool can_any_window_draw() const override; // events - virtual void process_events(); + void process_events() override; // icon - virtual void set_icon(const Ref<Image> &p_icon); + void set_icon(const Ref<Image> &p_icon) override; // others - virtual void swap_buffers(); + bool get_swap_cancel_ok() override; + void swap_buffers() override; static void register_javascript_driver(); DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index a55c9d3f48..f6db10fbbd 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -1,28 +1,11 @@ import os - -def parse_config(): - em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten") - if not os.path.exists(em_config_file): - raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file) - - normalized = {} - em_config = {} - with open(em_config_file) as f: - try: - # Emscripten configuration file is a Python file with simple assignments. - exec(f.read(), em_config) - except StandardError as e: - raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) - normalized["EMCC_ROOT"] = em_config.get("EMSCRIPTEN_ROOT") - normalized["NODE_JS"] = em_config.get("NODE_JS") - normalized["CLOSURE_BIN"] = os.path.join(normalized["EMCC_ROOT"], "node_modules", ".bin", "google-closure-compiler") - return normalized +from SCons.Util import WhereIs def run_closure_compiler(target, source, env, for_signature): - cfg = parse_config() - cmd = [cfg["NODE_JS"], cfg["CLOSURE_BIN"]] + closure_bin = os.path.join(os.path.dirname(WhereIs("emcc")), "node_modules", ".bin", "google-closure-compiler") + cmd = [WhereIs("node"), closure_bin] cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"]) for f in env["JSEXTERNS"]: cmd.extend(["--externs", f.get_abspath()]) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index d709422abb..05a11701c0 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -33,6 +33,7 @@ Function('return this')()['Engine'] = (function() { this.resizeCanvasOnStart = false; this.onExecute = null; this.onExit = null; + this.persistentPaths = []; }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { @@ -56,12 +57,14 @@ Function('return this')()['Engine'] = (function() { config['locateFile'] = Utils.createLocateRewrite(loadPath); config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); Godot(config).then(function(module) { - me.rtenv = module; - if (unloadAfterInit) { - unload(); - } - resolve(); - config = null; + module['initFS'](me.persistentPaths).then(function(fs_err) { + me.rtenv = module; + if (unloadAfterInit) { + unload(); + } + resolve(); + config = null; + }); }); }); return initPromise; @@ -118,10 +121,11 @@ Function('return this')()['Engine'] = (function() { me.rtenv['noExitRuntime'] = true; me.rtenv['onExecute'] = me.onExecute; me.rtenv['onExit'] = function(code) { + me.rtenv['deinitFS'](); if (me.onExit) me.onExit(code); me.rtenv = null; - } + }; return new Promise(function(resolve, reject) { preloader.preloadedFiles.forEach(function(file) { me.rtenv['copyToFS'](file.path, file.buffer); @@ -207,18 +211,28 @@ Function('return this')()['Engine'] = (function() { if (this.rtenv) this.rtenv.onExecute = onExecute; this.onExecute = onExecute; - } + }; Engine.prototype.setOnExit = function(onExit) { this.onExit = onExit; - } + }; Engine.prototype.copyToFS = function(path, buffer) { if (this.rtenv == null) { throw new Error("Engine must be inited before copying files"); } this.rtenv['copyToFS'](path, buffer); - } + }; + + Engine.prototype.setPersistentPaths = function(persistentPaths) { + this.persistentPaths = persistentPaths; + }; + + Engine.prototype.requestQuit = function() { + if (this.rtenv) { + this.rtenv['request_quit'](); + } + }; // Closure compiler exported engine methods. /** @export */ @@ -241,5 +255,7 @@ Function('return this')()['Engine'] = (function() { Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute; Engine.prototype['setOnExit'] = Engine.prototype.setOnExit; Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; + Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths; + Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; return Engine; })(); diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index 6a3a977cfb..a83ff44d20 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -124,6 +124,9 @@ public: String s = "HTTP/1.1 200 OK\r\n"; s += "Connection: Close\r\n"; s += "Content-Type: " + ctype + "\r\n"; + s += "Access-Control-Allow-Origin: *\r\n"; + s += "Cross-Origin-Opener-Policy: same-origin\r\n"; + s += "Cross-Origin-Embedder-Policy: require-corp\r\n"; s += "\r\n"; CharString cs = s.utf8(); Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); @@ -258,6 +261,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re current_line = current_line.replace("$GODOT_BASENAME", p_name); current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name")); current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include")); + current_line = current_line.replace("$GODOT_FULL_WINDOW", p_preset->get("html/full_window_size") ? "true" : "false"); current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false"); current_line = current_line.replace("$GODOT_ARGS", flags_json); str_export += current_line + "\n"; @@ -291,6 +295,7 @@ void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_op r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/full_window_size"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); } diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h new file mode 100644 index 0000000000..f7f26e5262 --- /dev/null +++ b/platform/javascript/godot_audio.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* godot_audio.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 GODOT_AUDIO_H +#define GODOT_AUDIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +extern int godot_audio_is_available(); + +extern int godot_audio_init(int p_mix_rate, int p_latency); +extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); + +extern void godot_audio_start(float *r_buffer_ptr); +extern void godot_audio_resume(); +extern void godot_audio_finish_async(); + +extern float godot_audio_get_latency(); + +extern void godot_audio_capture_start(); +extern void godot_audio_capture_stop(); + +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_AUDIO_H */ diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index f697887f08..01722c4bc8 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -34,10 +34,22 @@ #include "platform/javascript/os_javascript.h" #include <emscripten/emscripten.h> +#include <stdlib.h> static OS_JavaScript *os = nullptr; static uint64_t target_ticks = 0; +extern "C" EMSCRIPTEN_KEEPALIVE void _request_quit_callback(char *p_filev[], int p_filec) { + DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); + if (ds) { + Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + void exit_callback() { emscripten_cancel_main_loop(); // After this, we can exit! Main::cleanup(); @@ -76,12 +88,27 @@ void main_loop_callback() { /* clang-format on */ os->get_main_loop()->finish(); os->finalize_async(); // Will add all the async finish functions. + /* clang-format off */ EM_ASM({ Promise.all(Module.async_finish).then(function() { Module.async_finish = []; + return new Promise(function(accept, reject) { + if (!Module.idbfs) { + accept(); + return; + } + FS.syncfs(function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + accept(); + }); + }); + }).then(function() { ccall("cleanup_after_sync", null, []); }); }); + /* clang-format on */ } } @@ -89,31 +116,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { emscripten_set_main_loop(exit_callback, -1, false); } -extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { - String idbfs_err = String::utf8(p_idbfs_err); - if (!idbfs_err.empty()) { - print_line("IndexedDB not available: " + idbfs_err); - } - os->set_idb_available(idbfs_err.empty()); - // TODO: Check error return value. - Main::setup2(); // Manual second phase. - // Ease up compatibility. - ResourceLoader::set_abort_on_missing_resources(false); - Main::start(); - os->get_main_loop()->init(); - // Immediately run the first iteration. - // We are inside an animation frame, we want to immediately draw on the newly setup canvas. - main_loop_callback(); - emscripten_resume_main_loop(); -} - +/// When calling main, it is assumed FS is setup and synced. int main(int argc, char *argv[]) { - // Create and mount userfs immediately. - EM_ASM({ - FS.mkdir('/userfs'); - FS.mount(IDBFS, {}, '/userfs'); - }); - // Configure locale. char locale_ptr[16]; /* clang-format off */ @@ -131,26 +135,30 @@ int main(int argc, char *argv[]) { /* clang-format on */ os = new OS_JavaScript(); + os->set_idb_available((bool)EM_ASM_INT({ return Module.idbfs })); // We must override main when testing is enabled TEST_MAIN_OVERRIDE - Main::setup(argv[0], argc - 1, &argv[1], false); - emscripten_set_main_loop(main_loop_callback, -1, false); - emscripten_pause_main_loop(); // Will need to wait for FS sync. + Main::setup(argv[0], argc - 1, &argv[1]); - // Sync from persistent state into memory and then - // run the 'main_after_fs_sync' function. + // Ease up compatibility. + ResourceLoader::set_abort_on_missing_resources(false); + + Main::start(); + os->get_main_loop()->init(); + // Expose method for requesting quit. /* clang-format off */ EM_ASM({ - FS.syncfs(true, function(err) { - requestAnimationFrame(function() { - ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]); - }); - }); + Module['request_quit'] = function() { + ccall("_request_quit_callback", null, []); + }; }); /* clang-format on */ + emscripten_set_main_loop(main_loop_callback, -1, false); + // Immediately run the first iteration. + // We are inside an animation frame, we want to immediately draw on the newly setup canvas. + main_loop_callback(); return 0; - // Continued async in main_after_fs_sync() from the syncfs() callback. } diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js new file mode 100644 index 0000000000..4e7f3e2af5 --- /dev/null +++ b/platform/javascript/native/library_godot_audio.js @@ -0,0 +1,173 @@ +/*************************************************************************/ +/* library_godot_audio.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ +var GodotAudio = { + + $GodotAudio: { + + ctx: null, + input: null, + script: null, + }, + + godot_audio_is_available__proxy: 'sync', + godot_audio_is_available: function () { + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; + }, + + godot_audio_init: function(mix_rate, latency) { + GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: mix_rate, + // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. + }); + return GodotAudio.ctx.destination.channelCount; + }, + + godot_audio_create_processor: function(buffer_length, channel_count) { + GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudio.script.connect(GodotAudio.ctx.destination); + return GodotAudio.script.bufferSize; + }, + + godot_audio_start: function(buffer_ptr) { + var audioDriverProcessStart = cwrap('audio_driver_process_start'); + var audioDriverProcessEnd = cwrap('audio_driver_process_end'); + var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); + GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { + audioDriverProcessStart(); + + var input = audioProcessingEvent.inputBuffer; + var output = audioProcessingEvent.outputBuffer; + var internalBuffer = HEAPF32.subarray( + buffer_ptr / HEAPF32.BYTES_PER_ELEMENT, + buffer_ptr / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); + for (var channel = 0; channel < output.numberOfChannels; channel++) { + var outputData = output.getChannelData(channel); + // Loop through samples. + for (var sample = 0; sample < outputData.length; sample++) { + outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; + } + } + + if (GodotAudio.input) { + var inputDataL = input.getChannelData(0); + var inputDataR = input.getChannelData(1); + for (var i = 0; i < inputDataL.length; i++) { + audioDriverProcessCapture(inputDataL[i]); + audioDriverProcessCapture(inputDataR[i]); + } + } + audioDriverProcessEnd(); + }; + }, + + godot_audio_resume: function() { + if (GodotAudio.ctx && GodotAudio.ctx.state != 'running') { + GodotAudio.ctx.resume(); + } + }, + + godot_audio_finish_async: function() { + Module.async_finish.push(new Promise(function(accept, reject) { + if (!GodotAudio.ctx) { + setTimeout(accept, 0); + } else { + if (GodotAudio.script) { + GodotAudio.script.disconnect(); + GodotAudio.script = null; + } + if (GodotAudio.input) { + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + GodotAudio.ctx.close().then(function() { + accept(); + }).catch(function(e) { + accept(); + }); + GodotAudio.ctx = null; + } + })); + }, + + godot_audio_get_latency__proxy: 'sync', + godot_audio_get_latency: function() { + var latency = 0; + if (GodotAudio.ctx) { + if (GodotAudio.ctx.baseLatency) { + latency += GodotAudio.ctx.baseLatency; + } + if (GodotAudio.ctx.outputLatency) { + latency += GodotAudio.ctx.outputLatency; + } + } + return latency; + }, + + godot_audio_capture_start__proxy: 'sync', + godot_audio_capture_start: function() { + if (GodotAudio.input) { + return; // Already started. + } + function gotMediaInput(stream) { + GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); + GodotAudio.input.connect(GodotAudio.script); + } + + function gotMediaInputError(e) { + out(e); + } + + if (navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); + } else { + if (!navigator.getUserMedia) + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); + } + }, + + godot_audio_capture_stop__proxy: 'sync', + godot_audio_capture_stop: function() { + if (GodotAudio.input) { + const tracks = GodotAudio.input.mediaStream.getTracks(); + for (var i = 0; i < tracks.length; i++) { + tracks[i].stop(); + } + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + }, +}; + +autoAddDeps(GodotAudio, "$GodotAudio"); +mergeInto(LibraryManager.library, GodotAudio); diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js index 95585d26ae..8d0beba454 100644 --- a/platform/javascript/native/utils.js +++ b/platform/javascript/native/utils.js @@ -28,6 +28,53 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +Module['initFS'] = function(persistentPaths) { + Module.mount_points = ['/userfs'].concat(persistentPaths); + + function createRecursive(dir) { + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + } + + Module.mount_points.forEach(function(path) { + createRecursive(path); + FS.mount(IDBFS, {}, path); + }); + return new Promise(function(resolve, reject) { + FS.syncfs(true, function(err) { + if (err) { + Module.mount_points = []; + Module.idbfs = false; + console.log("IndexedDB not available: " + err.message); + } else { + Module.idbfs = true; + } + resolve(err); + }); + }); +}; + +Module['deinitFS'] = function() { + Module.mount_points.forEach(function(path) { + try { + FS.unmount(path); + } catch (e) { + console.log("Already unmounted", e); + } + if (Module.idbfs && IDBFS.dbs[path]) { + IDBFS.dbs[path].close(); + delete IDBFS.dbs[path]; + } + }); + Module.mount_points = []; +}; + Module['copyToFS'] = function(path, buffer) { var p = path.lastIndexOf("/"); var dir = "/"; @@ -37,7 +84,7 @@ Module['copyToFS'] = function(path, buffer) { try { FS.stat(dir); } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h + if (e.errno !== ERRNO_CODES.ENOENT) { throw e; } FS.mkdirTree(dir); @@ -202,3 +249,44 @@ Module.drop_handler = (function() { }); } })(); + +function EventHandlers() { + function Handler(target, event, method, capture) { + this.target = target; + this.event = event; + this.method = method; + this.capture = capture; + } + + var listeners = []; + + function has(target, event, method, capture) { + return listeners.findIndex(function(e) { + return e.target === target && e.event === event && e.method === method && e.capture == capture; + }) !== -1; + } + + this.add = function(target, event, method, capture) { + if (has(target, event, method, capture)) { + return; + } + listeners.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }; + + this.remove = function(target, event, method, capture) { + if (!has(target, event, method, capture)) { + return; + } + target.removeEventListener(event, method, capture); + }; + + this.clear = function() { + listeners.forEach(function(h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + listeners.length = 0; + }; +} + +Module.listeners = new EventHandlers(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 1ff4304bcf..cf5751f384 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -46,24 +46,6 @@ #include <emscripten.h> #include <stdlib.h> -bool OS_JavaScript::has_touchscreen_ui_hint() const { - /* clang-format off */ - return EM_ASM_INT_V( - return 'ontouchstart' in window; - ); - /* clang-format on */ -} - -// Audio - -int OS_JavaScript::get_audio_driver_count() const { - return 1; -} - -const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { - return "JavaScript"; -} - // Lifecycle void OS_JavaScript::initialize() { OS_Unix::initialize_core(); @@ -90,27 +72,24 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } -void OS_JavaScript::main_loop_callback() { - get_singleton()->main_loop_iterate(); +extern "C" EMSCRIPTEN_KEEPALIVE void _idb_synced() { + OS_JavaScript::get_singleton()->idb_is_syncing = false; } bool OS_JavaScript::main_loop_iterate() { - if (is_userfs_persistent() && sync_wait_time >= 0) { - int64_t current_time = get_ticks_msec(); - int64_t elapsed_time = current_time - last_sync_check_time; - last_sync_check_time = current_time; - - sync_wait_time -= elapsed_time; - - if (sync_wait_time < 0) { - /* clang-format off */ - EM_ASM( - FS.syncfs(function(error) { - if (error) { err('Failed to save IDB file system: ' + error.message); } - }); - ); - /* clang-format on */ - } + if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) { + idb_is_syncing = true; + idb_needs_sync = false; + /* clang-format off */ + EM_ASM({ + FS.syncfs(function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + ccall("_idb_synced", 'void', [], []); + }); + }); + /* clang-format on */ } DisplayServer::get_singleton()->process_events(); @@ -201,10 +180,6 @@ String OS_JavaScript::get_name() const { return "HTML5"; } -bool OS_JavaScript::can_draw() const { - return true; // Always? -} - String OS_JavaScript::get_user_data_dir() const { return "/userfs"; }; @@ -222,11 +197,17 @@ String OS_JavaScript::get_data_path() const { } void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { - OS_JavaScript *os = get_singleton(); - if (os->is_userfs_persistent() && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) { - os->last_sync_check_time = OS::get_singleton()->get_ticks_msec(); - // Wait five seconds in case more files are about to be closed. - os->sync_wait_time = 5000; + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (!(os->is_userfs_persistent() && (p_flags & FileAccess::WRITE))) { + return; // FS persistence is not working or we are not writing. + } + bool is_file_persistent = p_file.begins_with("/userfs"); +#ifdef TOOLS_ENABLED + // Hack for editor persistence (can we track). + is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/"); +#endif + if (is_file_persistent) { + os->idb_needs_sync = true; } } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 22234f9355..85551d708b 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -44,57 +44,52 @@ class OS_JavaScript : public OS_Unix { bool finalizing = false; bool idb_available = false; - int64_t sync_wait_time = -1; - int64_t last_sync_check_time = -1; + bool idb_needs_sync = false; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); protected: - virtual void initialize(); + void initialize() override; - virtual void set_main_loop(MainLoop *p_main_loop); - virtual void delete_main_loop(); + void set_main_loop(MainLoop *p_main_loop) override; + void delete_main_loop() override; - virtual void finalize(); + void finalize() override; - virtual bool _check_internal_feature_support(const String &p_feature); + bool _check_internal_feature_support(const String &p_feature) override; public: + bool idb_is_syncing = false; + // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); - virtual void initialize_joypads(); - - virtual bool has_touchscreen_ui_hint() const; - - virtual int get_audio_driver_count() const; - virtual const char *get_audio_driver_name(int p_driver) const; + void initialize_joypads() override; - virtual MainLoop *get_main_loop() const; + MainLoop *get_main_loop() const override; void finalize_async(); bool main_loop_iterate(); - virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr); - virtual Error kill(const ProcessID &p_pid); - virtual int get_process_id() const; + Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr) override; + Error kill(const ProcessID &p_pid) override; + int get_process_id() const override; - String get_executable_path() const; - virtual Error shell_open(String p_uri); - virtual String get_name() const; + String get_executable_path() const override; + Error shell_open(String p_uri) override; + String get_name() const override; // Override default OS implementation which would block the main thread with delay_usec. // Implemented in javascript_main.cpp loop callback instead. - virtual void add_frame_delay(bool p_can_draw) {} - virtual bool can_draw() const; + void add_frame_delay(bool p_can_draw) override {} - virtual String get_cache_path() const; - virtual String get_config_path() const; - virtual String get_data_path() const; - virtual String get_user_data_dir() const; + String get_cache_path() const override; + String get_config_path() const override; + String get_data_path() const override; + String get_user_data_dir() const override; void set_idb_available(bool p_idb_available); - virtual bool is_userfs_persistent() const; + bool is_userfs_persistent() const override; void resume_audio(); bool is_finalizing() { return finalizing; } diff --git a/platform/linuxbsd/SCsub b/platform/linuxbsd/SCsub index ae75a75830..6e43ffcedb 100644 --- a/platform/linuxbsd/SCsub +++ b/platform/linuxbsd/SCsub @@ -18,5 +18,5 @@ common_x11 = [ prog = env.add_program("#bin/godot", ["godot_linuxbsd.cpp"] + common_x11) -if (env["debug_symbols"] == "full" or env["debug_symbols"] == "yes") and env["separate_debug_symbols"]: +if env["debug_symbols"] == "yes" and env["separate_debug_symbols"]: env.AddPostAction(prog, run_in_subprocess(platform_linuxbsd_builders.make_debug_linuxbsd)) diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index b3553e961a..36c304b7f9 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -30,8 +30,8 @@ #include "crash_handler_linuxbsd.h" +#include "core/config/project_settings.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "main/main.h" #ifdef DEBUG_ENABLED @@ -67,7 +67,7 @@ static void handle_crash(int sig) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { for (size_t i = 1; i < size; i++) { @@ -109,7 +109,7 @@ static void handle_crash(int sig) { output.erase(output.length() - 1, 1); } - fprintf(stderr, "[%ld] %s (%ls)\n", (long int)i, fname, output.c_str()); + fprintf(stderr, "[%ld] %s (%s)\n", (long int)i, fname, output.utf8().get_data()); } free(strings); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 3eb4c44bc1..ea101e24dd 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -35,6 +35,11 @@ def can_build(): print("xinerama not found.. x11 disabled.") return False + x11_error = os.system("pkg-config xext --modversion > /dev/null ") + if x11_error: + print("xext not found.. x11 disabled.") + return False + x11_error = os.system("pkg-config xrandr --modversion > /dev/null ") if x11_error: print("xrandr not found.. x11 disabled.") @@ -68,7 +73,7 @@ def get_opts(): BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN))", False), BoolVariable("pulseaudio", "Detect and use PulseAudio", True), BoolVariable("udev", "Use udev for gamepad connection callbacks", False), - EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), + EnumVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", "yes", ("yes", "no")), BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), BoolVariable("touch", "Enable touch events", True), BoolVariable("execinfo", "Use libexecinfo on systems where glibc is not available", False), @@ -91,8 +96,6 @@ def configure(env): env.Prepend(CCFLAGS=["-Os"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "release_debug": @@ -103,8 +106,6 @@ def configure(env): env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "debug": @@ -194,6 +195,7 @@ def configure(env): env.ParseConfig("pkg-config x11 --cflags --libs") env.ParseConfig("pkg-config xcursor --cflags --libs") env.ParseConfig("pkg-config xinerama --cflags --libs") + env.ParseConfig("pkg-config xext --cflags --libs") env.ParseConfig("pkg-config xrandr --cflags --libs") env.ParseConfig("pkg-config xrender --cflags --libs") env.ParseConfig("pkg-config xi --cflags --libs") diff --git a/platform/linuxbsd/detect_prime_x11.cpp b/platform/linuxbsd/detect_prime_x11.cpp index 1e46d3222d..e5a9bb4737 100644 --- a/platform/linuxbsd/detect_prime_x11.cpp +++ b/platform/linuxbsd/detect_prime_x11.cpp @@ -33,8 +33,8 @@ #include "detect_prime.h" -#include "core/print_string.h" -#include "core/ustring.h" +#include "core/string/print_string.h" +#include "core/string/ustring.h" #include <stdlib.h> diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 4aec6d256c..35418116b4 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -32,17 +32,13 @@ #ifdef X11_ENABLED -#include "core/print_string.h" -#include "core/project_settings.h" +#include "core/config/project_settings.h" +#include "core/string/print_string.h" #include "detect_prime_x11.h" #include "key_mapping_x11.h" #include "main/main.h" #include "scene/resources/texture.h" -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif - #if defined(VULKAN_ENABLED) #include "servers/rendering/rasterizer_rd/rasterizer_rd.h" #endif @@ -54,6 +50,7 @@ #include <X11/Xatom.h> #include <X11/Xutil.h> #include <X11/extensions/Xinerama.h> +#include <X11/extensions/shape.h> // ICCCM #define WM_NormalState 1L // window normal state @@ -87,6 +84,13 @@ #define VALUATOR_TILTX 3 #define VALUATOR_TILTY 4 +//#define DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED +#ifdef DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED +#define DEBUG_LOG_X11(...) printf(__VA_ARGS__) +#else +#define DEBUG_LOG_X11(...) +#endif + static const double abs_resolution_mult = 10000.0; static const double abs_resolution_range_mult = 10.0; @@ -318,23 +322,19 @@ bool DisplayServerX11::_refresh_device_info() { } void DisplayServerX11::_flush_mouse_motion() { - while (true) { - if (XPending(x11_display) > 0) { - XEvent event; - XPeekEvent(x11_display, &event); - - if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { - XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; - - if (event_data->evtype == XI_RawMotion) { - XNextEvent(x11_display, &event); - } else { - break; - } - } else { - break; + // Block events polling while flushing motion events. + MutexLock mutex_lock(events_mutex); + + for (uint32_t event_index = 0; event_index < polled_events.size(); ++event_index) { + XEvent &event = polled_events[event_index]; + if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { + XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; + if (event_data->evtype == XI_RawMotion) { + XFreeEventData(x11_display, &event.xcookie); + polled_events.remove(event_index--); + continue; } - } else { + XFreeEventData(x11_display, &event.xcookie); break; } } @@ -447,12 +447,25 @@ int DisplayServerX11::mouse_get_button_state() const { void DisplayServerX11::clipboard_set(const String &p_text) { _THREAD_SAFE_METHOD_ - internal_clipboard = p_text; + { + // The clipboard content can be accessed while polling for events. + MutexLock mutex_lock(events_mutex); + internal_clipboard = p_text; + } + XSetSelectionOwner(x11_display, XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, CurrentTime); XSetSelectionOwner(x11_display, XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, CurrentTime); } -static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard, Atom target) { +Bool DisplayServerX11::_predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg) { + if (event->type == SelectionNotify && event->xselection.requestor == *(Window *)arg) { + return True; + } else { + return False; + } +} + +String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const { String ret; Atom type; @@ -460,23 +473,27 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x int format, result; unsigned long len, bytes_left, dummy; unsigned char *data; - Window Sown = XGetSelectionOwner(x11_display, p_source); + Window selection_owner = XGetSelectionOwner(x11_display, p_source); - if (Sown == x11_window) { - return p_internal_clipboard; - }; + if (selection_owner == x11_window) { + return internal_clipboard; + } - if (Sown != None) { - XConvertSelection(x11_display, p_source, target, selection, - x11_window, CurrentTime); - XFlush(x11_display); - while (true) { + if (selection_owner != None) { + { + // Block events polling while processing selection events. + MutexLock mutex_lock(events_mutex); + + XConvertSelection(x11_display, p_source, target, selection, + x11_window, CurrentTime); + + XFlush(x11_display); + + // Blocking wait for predicate to be True + // and remove the event from the queue. XEvent event; - XNextEvent(x11_display, &event); - if (event.type == SelectionNotify && event.xselection.requestor == x11_window) { - break; - }; - }; + XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window); + } // // Do not get any data, see how much data is there @@ -510,14 +527,14 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x return ret; } -static String _clipboard_get(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard) { +String DisplayServerX11::_clipboard_get(Atom p_source, Window x11_window) const { String ret; Atom utf8_atom = XInternAtom(x11_display, "UTF8_STRING", True); if (utf8_atom != None) { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, utf8_atom); + ret = _clipboard_get_impl(p_source, x11_window, utf8_atom); } - if (ret == "") { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, XA_STRING); + if (ret.empty()) { + ret = _clipboard_get_impl(p_source, x11_window, XA_STRING); } return ret; } @@ -526,11 +543,11 @@ String DisplayServerX11::clipboard_get() const { _THREAD_SAFE_METHOD_ String ret; - ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); + ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window); - if (ret == "") { - ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); - }; + if (ret.empty()) { + ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window); + } return ret; } @@ -701,6 +718,8 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { WindowData &wd = windows[p_id]; + DEBUG_LOG_X11("delete_sub_window: %lu (%u) \n", wd.x11_window, p_id); + while (wd.transient_children.size()) { window_set_transient(wd.transient_children.front()->get(), INVALID_WINDOW_ID); } @@ -718,6 +737,7 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { XDestroyWindow(x11_display, wd.x11_window); if (wd.xic) { XDestroyIC(wd.xic); + wd.xic = nullptr; } windows.erase(p_id); @@ -737,15 +757,31 @@ ObjectID DisplayServerX11::window_get_attached_instance_id(WindowID p_window) co } DisplayServerX11::WindowID DisplayServerX11::get_window_at_screen_position(const Point2i &p_position) const { -#warning This is an incorrect implementation, if windows overlap, it should return the topmost visible one or none if occluded by a foreign window - + WindowID found_window = INVALID_WINDOW_ID; + WindowID parent_window = INVALID_WINDOW_ID; + unsigned int focus_order = 0; for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - Rect2i win_rect = Rect2i(window_get_position(E->key()), window_get_size(E->key())); + const WindowData &wd = E->get(); + + // Discard windows with no focus. + if (wd.focus_order == 0) { + continue; + } + + // Find topmost window which contains the given position. + WindowID window_id = E->key(); + Rect2i win_rect = Rect2i(window_get_position(window_id), window_get_size(window_id)); if (win_rect.has_point(p_position)) { - return E->key(); + // For siblings, pick the window which was focused last. + if ((parent_window != wd.transient_parent) || (wd.focus_order > focus_order)) { + found_window = window_id; + parent_window = wd.transient_parent; + focus_order = wd.focus_order; + } } } - return INVALID_WINDOW_ID; + + return found_window; } void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window) { @@ -761,6 +797,38 @@ void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)p_title.utf8().get_data(), p_title.utf8().length()); } +void DisplayServerX11::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + const WindowData &wd = windows[p_window]; + + int event_base, error_base; + const Bool ext_okay = XShapeQueryExtension(x11_display, &event_base, &error_base); + if (ext_okay) { + Region region; + if (p_region.size() == 0) { + region = XCreateRegion(); + XRectangle rect; + rect.x = 0; + rect.y = 0; + rect.width = window_get_real_size(p_window).x; + rect.height = window_get_real_size(p_window).y; + XUnionRectWithRegion(&rect, region, region); + } else { + XPoint *points = (XPoint *)memalloc(sizeof(XPoint) * p_region.size()); + for (int i = 0; i < p_region.size(); i++) { + points[i].x = p_region[i].x; + points[i].y = p_region[i].y; + } + region = XPolygonRegion(points, p_region.size(), EvenOddRule); + memfree(points); + } + XShapeCombineRegion(x11_display, wd.x11_window, ShapeInput, 0, 0, region, ShapeSet); + XDestroyRegion(region); + } +} + void DisplayServerX11::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -854,24 +922,34 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd_window = windows[p_window]; - ERR_FAIL_COND(wd_window.transient_parent == p_parent); + WindowID prev_parent = wd_window.transient_parent; + ERR_FAIL_COND(prev_parent == p_parent); ERR_FAIL_COND_MSG(wd_window.on_top, "Windows with the 'on top' can't become transient."); if (p_parent == INVALID_WINDOW_ID) { //remove transient - ERR_FAIL_COND(wd_window.transient_parent == INVALID_WINDOW_ID); - ERR_FAIL_COND(!windows.has(wd_window.transient_parent)); + ERR_FAIL_COND(prev_parent == INVALID_WINDOW_ID); + ERR_FAIL_COND(!windows.has(prev_parent)); - WindowData &wd_parent = windows[wd_window.transient_parent]; + WindowData &wd_parent = windows[prev_parent]; wd_window.transient_parent = INVALID_WINDOW_ID; wd_parent.transient_children.erase(p_window); XSetTransientForHint(x11_display, wd_window.x11_window, None); + + // Set focus to parent sub window to avoid losing all focus with nested menus. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd_window.menu_type && !wd_window.no_focus) { + if (!wd_parent.no_focus) { + XSetInputFocus(x11_display, wd_parent.x11_window, RevertToPointerRoot, CurrentTime); + } + } } else { ERR_FAIL_COND(!windows.has(p_parent)); - ERR_FAIL_COND_MSG(wd_window.transient_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); + ERR_FAIL_COND_MSG(prev_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); WindowData &wd_parent = windows[p_parent]; wd_window.transient_parent = p_parent; @@ -881,6 +959,46 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent } } +// Helper method. Assumes that the window id has already been checked and exists. +void DisplayServerX11::_update_size_hints(WindowID p_window) { + WindowData &wd = windows[p_window]; + WindowMode window_mode = window_get_mode(p_window); + XSizeHints *xsh = XAllocSizeHints(); + + // Always set the position and size hints - they should be synchronized with the actual values after the window is mapped anyway + xsh->flags |= PPosition | PSize; + xsh->x = wd.position.x; + xsh->y = wd.position.y; + xsh->width = wd.size.width; + xsh->height = wd.size.height; + + if (window_mode == WINDOW_MODE_FULLSCREEN) { + // Do not set any other hints to prevent the window manager from ignoring the fullscreen flags + } else if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { + // If resizing is disabled, use the forced size + xsh->flags |= PMinSize | PMaxSize; + xsh->min_width = wd.size.x; + xsh->max_width = wd.size.x; + xsh->min_height = wd.size.y; + xsh->max_height = wd.size.y; + } else { + // Otherwise, just respect min_size and max_size + if (wd.min_size != Size2i()) { + xsh->flags |= PMinSize; + xsh->min_width = wd.min_size.x; + xsh->min_height = wd.min_size.y; + } + if (wd.max_size != Size2i()) { + xsh->flags |= PMaxSize; + xsh->max_width = wd.max_size.x; + xsh->max_height = wd.max_size.y; + } + } + + XSetWMNormalHints(x11_display, wd.x11_window, xsh); + XFree(xsh); +} + Point2i DisplayServerX11::window_get_position(WindowID p_window) const { _THREAD_SAFE_METHOD_ @@ -934,25 +1052,8 @@ void DisplayServerX11::window_set_max_size(const Size2i p_size, WindowID p_windo } wd.max_size = p_size; - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - - XFlush(x11_display); - } + _update_size_hints(p_window); + XFlush(x11_display); } Size2i DisplayServerX11::window_get_max_size(WindowID p_window) const { @@ -976,25 +1077,8 @@ void DisplayServerX11::window_set_min_size(const Size2i p_size, WindowID p_windo } wd.min_size = p_size; - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - - XFlush(x11_display); - } + _update_size_hints(p_window); + XFlush(x11_display); } Size2i DisplayServerX11::window_get_min_size(WindowID p_window) const { @@ -1027,37 +1111,15 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) { int old_w = xwa.width; int old_h = xwa.height; - // If window resizable is disabled we need to update the attributes first - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + // Update our videomode width and height + wd.size = size; + + // Update the size hints first to make sure the window size can be set + _update_size_hints(p_window); // Resize the window XResizeWindow(x11_display, wd.x11_window, size.x, size.y); - // Update our videomode width and height - wd.size = size; - for (int timeout = 0; timeout < 50; ++timeout) { XSync(x11_display, False); XGetWindowAttributes(x11_display, wd.x11_window, &xwa); @@ -1213,14 +1275,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); } - if (p_enabled && window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { + if (p_enabled) { // Set the window as resizable to prevent window managers to ignore the fullscreen state flag. - XSizeHints *xsh; - - xsh = XAllocSizeHints(); - xsh->flags = 0L; - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + _update_size_hints(p_window); } // Using EWMH -- Extended Window Manager Hints @@ -1248,30 +1305,7 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { if (!p_enabled) { // Reset the non-resizable flags if we un-set these before. - Size2i size = window_get_size(p_window); - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + _update_size_hints(p_window); // put back or remove decorations according to the last set borderless state Hints hints; @@ -1329,13 +1363,13 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { } break; case WINDOW_MODE_FULLSCREEN: { //Remove full-screen + wd.fullscreen = false; + _set_wm_fullscreen(p_window, false); //un-maximize required for always on top bool on_top = window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window); - wd.fullscreen = false; - window_set_position(wd.last_position_before_fs, p_window); if (on_top) { @@ -1381,15 +1415,16 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { } break; case WINDOW_MODE_FULLSCREEN: { wd.last_position_before_fs = wd.position; + if (window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window)) { _set_wm_maximized(p_window, true); } - _set_wm_fullscreen(p_window, true); + wd.fullscreen = true; + _set_wm_fullscreen(p_window, true); } break; case WINDOW_MODE_MAXIMIZED: { _set_wm_maximized(p_window, true); - } break; } } @@ -1456,37 +1491,11 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo switch (p_flag) { case WINDOW_FLAG_RESIZE_DISABLED: { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (p_enabled) { - Size2i size = window_get_size(p_window); - - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - wd.resize_disabled = p_enabled; - XFlush(x11_display); + _update_size_hints(p_window); + XFlush(x11_display); } break; case WINDOW_FLAG_BORDERLESS: { Hints hints; @@ -1654,10 +1663,16 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win return; } + // Block events polling while changing input focus + // because it triggers some event polling internally. if (p_active) { - XSetICFocus(wd.xic); + { + MutexLock mutex_lock(events_mutex); + XSetICFocus(wd.xic); + } window_set_ime_position(wd.im_position, p_window); } else { + MutexLock mutex_lock(events_mutex); XUnsetICFocus(wd.xic); } } @@ -1678,7 +1693,14 @@ void DisplayServerX11::window_set_ime_position(const Point2i &p_pos, WindowID p_ spot.x = short(p_pos.x); spot.y = short(p_pos.y); XVaNestedList preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, nullptr); - XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + + { + // Block events polling during this call + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + } + XFree(preedit_attr); } @@ -1998,7 +2020,7 @@ unsigned int DisplayServerX11::_get_mouse_button_state(unsigned int p_x11_button return last_button_state; } -void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo) { +void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo) { WindowData wd = windows[p_window]; // X11 functions don't know what const is XKeyEvent *xkeyevent = p_event; @@ -2135,7 +2157,7 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, /* Phase 4, determine if event must be filtered */ // This seems to be a side-effect of using XIM. - // XEventFilter looks like a core X11 function, + // XFilterEvent looks like a core X11 function, // but it's actually just used to see if we must // ignore a deadkey, or events XIM determines // must not reach the actual gui. @@ -2169,17 +2191,16 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, // Echo characters in X11 are a keyrelease and a keypress // one after the other with the (almot) same timestamp. - // To detect them, i use XPeekEvent and check that their - // difference in time is below a threshold. + // To detect them, i compare to the next event in list and + // check that their difference in time is below a threshold. if (xkeyevent->type != KeyPress) { p_echo = false; // make sure there are events pending, // so this call won't block. - if (XPending(x11_display) > 0) { - XEvent peek_event; - XPeekEvent(x11_display, &peek_event); + if (p_event_index + 1 < p_events.size()) { + XEvent &peek_event = p_events[p_event_index + 1]; // I'm using a threshold of 5 msecs, // since sometimes there seems to be a little @@ -2194,9 +2215,9 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, KeySym rk; XLookupString((XKeyEvent *)&peek_event, str, 256, &rk, nullptr); if (rk == keysym_keycode) { - XEvent event; - XNextEvent(x11_display, &event); //erase next event - _handle_key_event(p_window, (XKeyEvent *)&event, true); + // Consume to next event. + ++p_event_index; + _handle_key_event(p_window, (XKeyEvent *)&peek_event, p_events, p_event_index, true); return; //ignore current, echo next } } @@ -2251,6 +2272,66 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, Input::get_singleton()->accumulate_input_event(k); } +void DisplayServerX11::_handle_selection_request_event(XSelectionRequestEvent *p_event) { + XEvent respond; + if (p_event->target == XInternAtom(x11_display, "UTF8_STRING", 0) || + p_event->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || + p_event->target == XInternAtom(x11_display, "TEXT", 0) || + p_event->target == XA_STRING || + p_event->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || + p_event->target == XInternAtom(x11_display, "text/plain", 0)) { + // Directly using internal clipboard because we know our window + // is the owner during a selection request. + CharString clip = internal_clipboard.utf8(); + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + p_event->target, + 8, + PropModeReplace, + (unsigned char *)clip.get_data(), + clip.length()); + respond.xselection.property = p_event->property; + } else if (p_event->target == XInternAtom(x11_display, "TARGETS", 0)) { + Atom data[7]; + data[0] = XInternAtom(x11_display, "TARGETS", 0); + data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); + data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); + data[3] = XInternAtom(x11_display, "TEXT", 0); + data[4] = XA_STRING; + data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); + data[6] = XInternAtom(x11_display, "text/plain", 0); + + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char *)&data, + sizeof(data) / sizeof(data[0])); + respond.xselection.property = p_event->property; + + } else { + char *targetname = XGetAtomName(x11_display, p_event->target); + printf("No Target '%s'\n", targetname); + if (targetname) { + XFree(targetname); + } + respond.xselection.property = None; + } + + respond.xselection.type = SelectionNotify; + respond.xselection.display = p_event->display; + respond.xselection.requestor = p_event->requestor; + respond.xselection.selection = p_event->selection; + respond.xselection.target = p_event->target; + respond.xselection.time = p_event->time; + + XSendEvent(x11_display, p_event->requestor, True, NoEventMask, &respond); + XFlush(x11_display); +} + void DisplayServerX11::_xim_destroy_callback(::XIM im, ::XPointer client_data, ::XPointer call_data) { WARN_PRINT("Input method stopped"); @@ -2363,9 +2444,73 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev } } +void DisplayServerX11::_poll_events_thread(void *ud) { + DisplayServerX11 *display_server = (DisplayServerX11 *)ud; + display_server->_poll_events(); +} + +Bool DisplayServerX11::_predicate_all_events(Display *display, XEvent *event, XPointer arg) { + // Just accept all events. + return True; +} + +void DisplayServerX11::_poll_events() { + int x11_fd = ConnectionNumber(x11_display); + fd_set in_fds; + + while (!events_thread_done) { + XFlush(x11_display); + + FD_ZERO(&in_fds); + FD_SET(x11_fd, &in_fds); + + struct timeval tv; + tv.tv_usec = 0; + tv.tv_sec = 1; + + // Wait for next event or timeout. + int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv); + if (num_ready_fds < 0) { + ERR_PRINT("_poll_events: select error: " + itos(errno)); + } + + // Process events from the queue. + { + MutexLock mutex_lock(events_mutex); + + // Non-blocking wait for next event + // and remove it from the queue. + XEvent ev; + while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, nullptr)) { + // Check if the input manager wants to process the event. + if (XFilterEvent(&ev, None)) { + // Event has been filtered by the Input Manager, + // it has to be ignored and a new one will be received. + continue; + } + + // Handle selection request events directly in the event thread, because + // communication through the x server takes several events sent back and forth + // and we don't want to block other programs while processing only one each frame. + if (ev.type == SelectionRequest) { + _handle_selection_request_event(&(ev.xselectionrequest)); + continue; + } + + polled_events.push_back(ev); + } + } + } +} + void DisplayServerX11::process_events() { _THREAD_SAFE_METHOD_ +#ifdef DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED + static int frame = 0; + ++frame; +#endif + if (app_focused) { //verify that one of the windows has focus, else send focus out notification bool focus_found = false; @@ -2382,6 +2527,7 @@ void DisplayServerX11::process_events() { if (delta > 250) { //X11 can go between windows and have no focus for a while, when creating them or something else. Use this as safety to avoid unnecessary focus in/outs. if (OS::get_singleton()->get_main_loop()) { + DEBUG_LOG_X11("All focus lost, triggering NOTIFICATION_APPLICATION_FOCUS_OUT\n"); OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); } app_focused = false; @@ -2400,9 +2546,16 @@ void DisplayServerX11::process_events() { xi.tilt = Vector2(); xi.pressure_supported = false; - while (XPending(x11_display) > 0) { - XEvent event; - XNextEvent(x11_display, &event); + LocalVector<XEvent> events; + { + // Block events polling while flushing events. + MutexLock mutex_lock(events_mutex); + events = polled_events; + polled_events.clear(); + } + + for (uint32_t event_index = 0; event_index < events.size(); ++event_index) { + XEvent &event = events[event_index]; WindowID window_id = MAIN_WINDOW_ID; @@ -2414,10 +2567,6 @@ void DisplayServerX11::process_events() { } } - if (XFilterEvent(&event, None)) { - continue; - } - if (XGetEventData(x11_display, &event.xcookie)) { if (event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; @@ -2580,32 +2729,74 @@ void DisplayServerX11::process_events() { XFreeEventData(x11_display, &event.xcookie); switch (event.type) { - case Expose: + case MapNotify: { + DEBUG_LOG_X11("[%u] MapNotify window=%lu (%u) \n", frame, event.xmap.window, window_id); + + const WindowData &wd = windows[window_id]; + + // Set focus when menu window is started. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd.menu_type && !wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); + } + } break; + + case Expose: { + DEBUG_LOG_X11("[%u] Expose window=%lu (%u), count='%u' \n", frame, event.xexpose.window, window_id, event.xexpose.count); + Main::force_redraw(); - break; + } break; + + case NoExpose: { + DEBUG_LOG_X11("[%u] NoExpose drawable=%lu (%u) \n", frame, event.xnoexpose.drawable, window_id); - case NoExpose: windows[window_id].minimized = true; - break; + } break; case VisibilityNotify: { + DEBUG_LOG_X11("[%u] VisibilityNotify window=%lu (%u), state=%u \n", frame, event.xvisibility.window, window_id, event.xvisibility.state); + XVisibilityEvent *visibility = (XVisibilityEvent *)&event; windows[window_id].minimized = (visibility->state == VisibilityFullyObscured); } break; + case LeaveNotify: { + DEBUG_LOG_X11("[%u] LeaveNotify window=%lu (%u), mode='%u' \n", frame, event.xcrossing.window, window_id, event.xcrossing.mode); + if (!mouse_mode_grab) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_EXIT); } } break; + case EnterNotify: { + DEBUG_LOG_X11("[%u] EnterNotify window=%lu (%u), mode='%u' \n", frame, event.xcrossing.window, window_id, event.xcrossing.mode); + if (!mouse_mode_grab) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_ENTER); } } break; - case FocusIn: - windows[window_id].focused = true; - _send_window_event(windows[window_id], WINDOW_EVENT_FOCUS_IN); + + case FocusIn: { + DEBUG_LOG_X11("[%u] FocusIn window=%lu (%u), mode='%u' \n", frame, event.xfocus.window, window_id, event.xfocus.mode); + + WindowData &wd = windows[window_id]; + + wd.focused = true; + + if (wd.xic) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XSetICFocus(wd.xic); + } + + // Keep track of focus order for overlapping windows. + static unsigned int focus_order = 0; + wd.focus_order = ++focus_order; + + _send_window_event(wd, WINDOW_EVENT_FOCUS_IN); if (mouse_mode_grab) { // Show and update the cursor if confined and the window regained focus. @@ -2629,9 +2820,6 @@ void DisplayServerX11::process_events() { XIGrabDevice(x11_display, xi.touch_devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &xi.touch_event_mask); }*/ #endif - if (windows[window_id].xic) { - XSetICFocus(windows[window_id].xic); - } if (!app_focused) { if (OS::get_singleton()->get_main_loop()) { @@ -2639,12 +2827,24 @@ void DisplayServerX11::process_events() { } app_focused = true; } - break; + } break; + + case FocusOut: { + DEBUG_LOG_X11("[%u] FocusOut window=%lu (%u), mode='%u' \n", frame, event.xfocus.window, window_id, event.xfocus.mode); + + WindowData &wd = windows[window_id]; + + wd.focused = false; + + if (wd.xic) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XUnsetICFocus(wd.xic); + } - case FocusOut: - windows[window_id].focused = false; Input::get_singleton()->release_pressed_events(); - _send_window_event(windows[window_id], WINDOW_EVENT_FOCUS_OUT); + _send_window_event(wd, WINDOW_EVENT_FOCUS_OUT); if (mouse_mode_grab) { for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { @@ -2673,14 +2873,23 @@ void DisplayServerX11::process_events() { } xi.state.clear(); #endif - if (windows[window_id].xic) { - XSetICFocus(windows[window_id].xic); + } break; + + case ConfigureNotify: { + DEBUG_LOG_X11("[%u] ConfigureNotify window=%lu (%u), event=%lu, above=%lu, override_redirect=%u \n", frame, event.xconfigure.window, window_id, event.xconfigure.event, event.xconfigure.above, event.xconfigure.override_redirect); + + const WindowData &wd = windows[window_id]; + + // Set focus when menu window is re-used. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd.menu_type && !wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); } - break; - case ConfigureNotify: _window_changed(&event); - break; + } break; + case ButtonPress: case ButtonRelease: { /* exit in case of a mouse button press */ @@ -2707,7 +2916,18 @@ void DisplayServerX11::process_events() { mb->set_pressed((event.type == ButtonPress)); + const WindowData &wd = windows[window_id]; + if (event.type == ButtonPress) { + DEBUG_LOG_X11("[%u] ButtonPress window=%lu (%u), button_index=%u \n", frame, event.xbutton.window, window_id, mb->get_button_index()); + + // Ensure window focus on click. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (!wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); + } + uint64_t diff = OS::get_singleton()->get_ticks_usec() / 1000 - last_click_ms; if (mb->get_button_index() == last_click_button_index) { @@ -2726,6 +2946,33 @@ void DisplayServerX11::process_events() { last_click_ms += diff; last_click_pos = Point2i(event.xbutton.x, event.xbutton.y); } + } else { + DEBUG_LOG_X11("[%u] ButtonRelease window=%lu (%u), button_index=%u \n", frame, event.xbutton.window, window_id, mb->get_button_index()); + + if (!wd.focused) { + // Propagate the event to the focused window, + // because it's received only on the topmost window. + // Note: This is needed for drag & drop to work between windows, + // because the engine expects events to keep being processed + // on the same window dragging started. + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + const WindowData &wd_other = E->get(); + WindowID window_id_other = E->key(); + if (wd_other.focused) { + if (window_id_other != window_id) { + int x, y; + Window child; + XTranslateCoordinates(x11_display, wd.x11_window, wd_other.x11_window, event.xbutton.x, event.xbutton.y, &x, &y, &child); + + mb->set_window_id(window_id_other); + mb->set_position(Vector2(x, y)); + mb->set_global_position(mb->get_position()); + Input::get_singleton()->accumulate_input_event(mb); + } + break; + } + } + } } Input::get_singleton()->accumulate_input_event(mb); @@ -2743,11 +2990,11 @@ void DisplayServerX11::process_events() { break; } - if (XPending(x11_display) > 0) { - XEvent tevent; - XPeekEvent(x11_display, &tevent); - if (tevent.type == MotionNotify) { - XNextEvent(x11_display, &event); + if (event_index + 1 < events.size()) { + const XEvent &next_event = events[event_index + 1]; + if (next_event.type == MotionNotify) { + ++event_index; + event = next_event; } else { break; } @@ -2775,6 +3022,9 @@ void DisplayServerX11::process_events() { break; } + const WindowData &wd = windows[window_id]; + bool focused = wd.focused; + if (mouse_mode == MOUSE_MODE_CAPTURED) { if (xi.relative_motion.x == 0 && xi.relative_motion.y == 0) { break; @@ -2783,7 +3033,7 @@ void DisplayServerX11::process_events() { Point2i new_center = pos; pos = last_mouse_pos + xi.relative_motion; center = new_center; - do_mouse_warp = windows[window_id].focused; // warp the cursor if we're focused in + do_mouse_warp = focused; // warp the cursor if we're focused in } if (!last_mouse_pos_valid) { @@ -2825,14 +3075,11 @@ void DisplayServerX11::process_events() { } mm->set_tilt(xi.tilt); - // Make the absolute position integral so it doesn't look _too_ weird :) - Point2i posi(pos); - _get_key_modifier_state(event.xmotion.state, mm); mm->set_button_mask(mouse_get_button_state()); - mm->set_position(posi); - mm->set_global_position(posi); - Input::get_singleton()->set_mouse_position(posi); + mm->set_position(pos); + mm->set_global_position(pos); + Input::get_singleton()->set_mouse_position(pos); mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); mm->set_relative(rel); @@ -2843,8 +3090,32 @@ void DisplayServerX11::process_events() { // Don't propagate the motion event unless we have focus // this is so that the relative motion doesn't get messed up // after we regain focus. - if (windows[window_id].focused || !mouse_mode_grab) { + if (focused) { Input::get_singleton()->accumulate_input_event(mm); + } else { + // Propagate the event to the focused window, + // because it's received only on the topmost window. + // Note: This is needed for drag & drop to work between windows, + // because the engine expects events to keep being processed + // on the same window dragging started. + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + const WindowData &wd_other = E->get(); + if (wd_other.focused) { + int x, y; + Window child; + XTranslateCoordinates(x11_display, wd.x11_window, wd_other.x11_window, event.xmotion.x, event.xmotion.y, &x, &y, &child); + + Point2i pos_focused(x, y); + + mm->set_window_id(E->key()); + mm->set_position(pos_focused); + mm->set_global_position(pos_focused); + mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); + Input::get_singleton()->accumulate_input_event(mm); + + break; + } + } } } break; @@ -2854,67 +3125,7 @@ void DisplayServerX11::process_events() { // key event is a little complex, so // it will be handled in its own function. - _handle_key_event(window_id, (XKeyEvent *)&event); - } break; - case SelectionRequest: { - XSelectionRequestEvent *req; - XEvent e, respond; - e = event; - - req = &(e.xselectionrequest); - if (req->target == XInternAtom(x11_display, "UTF8_STRING", 0) || - req->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || - req->target == XInternAtom(x11_display, "TEXT", 0) || - req->target == XA_STRING || - req->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || - req->target == XInternAtom(x11_display, "text/plain", 0)) { - CharString clip = clipboard_get().utf8(); - XChangeProperty(x11_display, - req->requestor, - req->property, - req->target, - 8, - PropModeReplace, - (unsigned char *)clip.get_data(), - clip.length()); - respond.xselection.property = req->property; - } else if (req->target == XInternAtom(x11_display, "TARGETS", 0)) { - Atom data[7]; - data[0] = XInternAtom(x11_display, "TARGETS", 0); - data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); - data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); - data[3] = XInternAtom(x11_display, "TEXT", 0); - data[4] = XA_STRING; - data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); - data[6] = XInternAtom(x11_display, "text/plain", 0); - - XChangeProperty(x11_display, - req->requestor, - req->property, - XA_ATOM, - 32, - PropModeReplace, - (unsigned char *)&data, - sizeof(data) / sizeof(data[0])); - respond.xselection.property = req->property; - - } else { - char *targetname = XGetAtomName(x11_display, req->target); - printf("No Target '%s'\n", targetname); - if (targetname) { - XFree(targetname); - } - respond.xselection.property = None; - } - - respond.xselection.type = SelectionNotify; - respond.xselection.display = req->display; - respond.xselection.requestor = req->requestor; - respond.xselection.selection = req->selection; - respond.xselection.target = req->target; - respond.xselection.time = req->time; - XSendEvent(x11_display, req->requestor, True, NoEventMask, &respond); - XFlush(x11_display); + _handle_key_event(window_id, (XKeyEvent *)&event, events, event_index); } break; case SelectionNotify: @@ -3221,11 +3432,38 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u unsigned long valuemask = CWBorderPixel | CWColormap | CWEventMask; - WindowID id; + WindowID id = window_id_counter++; + WindowData &wd = windows[id]; + + if ((id != MAIN_WINDOW_ID) && (p_flags & WINDOW_FLAG_BORDERLESS_BIT)) { + wd.menu_type = true; + } + + if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { + wd.menu_type = true; + wd.no_focus = true; + } + + // Setup for menu subwindows: + // - override_redirect forces the WM not to interfere with the window, to avoid delays due to + // handling decorations and placement. + // On the other hand, focus changes need to be handled manually when this is set. + // - save_under is a hint for the WM to keep the content of windows behind to avoid repaint. + if (wd.menu_type) { + windowAttributes.override_redirect = True; + windowAttributes.save_under = True; + valuemask |= CWOverrideRedirect | CWSaveUnder; + } + { - WindowData wd; wd.x11_window = XCreateWindow(x11_display, RootWindow(x11_display, visualInfo->screen), p_rect.position.x, p_rect.position.y, p_rect.size.width > 0 ? p_rect.size.width : 1, p_rect.size.height > 0 ? p_rect.size.height : 1, 0, visualInfo->depth, InputOutput, visualInfo->visual, valuemask, &windowAttributes); + // Enable receiving notification when the window is initialized (MapNotify) + // so the focus can be set at the right time. + if (wd.menu_type && !wd.no_focus) { + XSelectInput(x11_display, wd.x11_window, StructureNotifyMask); + } + //associate PID // make PID known to X11 { @@ -3282,6 +3520,10 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u XChangeProperty(x11_display, wd.x11_window, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); if (xim && xim_style) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + wd.xic = XCreateIC(xim, XNInputStyle, xim_style, XNClientWindow, wd.x11_window, XNFocusWindow, wd.x11_window, (char *)nullptr); if (XGetICValues(wd.xic, XNFilterEvents, &im_event_mask, nullptr) != nullptr) { WARN_PRINT("XGetICValues couldn't obtain XNFilterEvents value"); @@ -3300,87 +3542,30 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u _update_context(wd); - id = window_id_counter++; - - windows[id] = wd; - - { - if (p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = p_rect.size.width; - xsh->max_width = p_rect.size.width; - xsh->min_height = p_rect.size.height; - xsh->max_height = p_rect.size.height; - - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - } - - bool make_utility = false; - - if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) { - Hints hints; - Atom property; - hints.flags = 2; - hints.decorations = 0; - property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); - - make_utility = true; - } - if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { - make_utility = true; - } - - if (make_utility) { - //this one seems to disable the fade animations for regular windows - //but has the drawback that will not get focus by default, so - //we need to force it, unless no focus requested - - Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_UTILITY", False); - Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); - - if (!(p_flags & WINDOW_FLAG_NO_FOCUS_BIT)) { - //but as utility appears unfocused, it needs to be forcefuly focused, unless no focus requested - XEvent xev; - Atom net_active_window = XInternAtom(x11_display, "_NET_ACTIVE_WINDOW", False); - - memset(&xev, 0, sizeof(xev)); - xev.type = ClientMessage; - xev.xclient.window = wd.x11_window; - xev.xclient.message_type = net_active_window; - xev.xclient.format = 32; - xev.xclient.data.l[0] = 1; - xev.xclient.data.l[1] = CurrentTime; - - XSendEvent(x11_display, DefaultRootWindow(x11_display), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev); - } - } else { - Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", False); - Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); - } + if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) { + Hints hints; + Atom property; + hints.flags = 2; + hints.decorations = 0; + property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); } - if (id != MAIN_WINDOW_ID) { - XSizeHints my_hints = XSizeHints(); + if (wd.menu_type) { + // Set Utility type to disable fade animations. + Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - my_hints.flags = PPosition | PSize; /* I want to specify position and size */ - my_hints.x = p_rect.position.x; /* The origin and size coords I want */ - my_hints.y = p_rect.position.y; - my_hints.width = p_rect.size.width; - my_hints.height = p_rect.size.height; + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + } else { + Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", False); + Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - XSetNormalHints(x11_display, wd.x11_window, &my_hints); - XMoveWindow(x11_display, wd.x11_window, p_rect.position.x, p_rect.position.y); + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); } + _update_size_hints(id); + #if defined(VULKAN_ENABLED) if (context_vulkan) { Error err = context_vulkan->window_create(id, wd.x11_window, x11_display, p_rect.size.width, p_rect.size.height); @@ -3397,8 +3582,6 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u XFree(visualInfo); } - WindowData &wd = windows[id]; - window_set_mode(p_mode, id); //sync size @@ -3492,7 +3675,12 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode xrandr_handle = dlopen("libXrandr.so.2", RTLD_LAZY); if (!xrandr_handle) { err = dlerror(); - fprintf(stderr, "could not load libXrandr.so.2, Error: %s\n", err); + // For some arcane reason, NetBSD now ships libXrandr.so.3 while the rest of the world has libXrandr.so.2... + // In case this happens for other X11 platforms in the future, let's give it a try too before failing. + xrandr_handle = dlopen("libXrandr.so.3", RTLD_LAZY); + if (!xrandr_handle) { + fprintf(stderr, "could not load libXrandr.so.2, Error: %s\n", err); + } } else { XRRQueryVersion(x11_display, &xrandr_major, &xrandr_minor); if (((xrandr_major << 8) | xrandr_minor) >= 0x0105) { @@ -3838,12 +4026,19 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } } + events_thread = Thread::create(_poll_events_thread, this); + _update_real_mouse_position(windows[MAIN_WINDOW_ID]); r_error = OK; } DisplayServerX11::~DisplayServerX11() { + events_thread_done = true; + Thread::wait_to_finish(events_thread); + memdelete(events_thread); + events_thread = nullptr; + //destroy all windows for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { #ifdef VULKAN_ENABLED @@ -3852,11 +4047,13 @@ DisplayServerX11::~DisplayServerX11() { } #endif - if (E->get().xic) { - XDestroyIC(E->get().xic); + WindowData &wd = E->get(); + if (wd.xic) { + XDestroyIC(wd.xic); + wd.xic = nullptr; } - XUnmapWindow(x11_display, E->get().x11_window); - XDestroyWindow(x11_display, E->get().x11_window); + XUnmapWindow(x11_display, wd.x11_window); + XDestroyWindow(x11_display, wd.x11_window); } //destroy drivers diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index 0ba1359145..9ff28b9ed2 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -36,6 +36,7 @@ #include "servers/display_server.h" #include "core/input/input.h" +#include "core/templates/local_vector.h" #include "drivers/alsa/audio_driver_alsa.h" #include "drivers/alsamidi/midi_driver_alsamidi.h" #include "drivers/pulseaudio/audio_driver_pulseaudio.h" @@ -132,6 +133,9 @@ class DisplayServerX11 : public DisplayServer { ObjectID instance_id; + bool menu_type = false; + bool no_focus = false; + //better to guess on the fly, given WM can change it //WindowMode mode; bool fullscreen = false; //OS can't exit from this mode @@ -141,6 +145,8 @@ class DisplayServerX11 : public DisplayServer { Vector2i last_position_before_fs; bool focused = false; bool minimized = false; + + unsigned int focus_order = 0; }; Map<WindowID, WindowData> windows; @@ -197,7 +203,11 @@ class DisplayServerX11 : public DisplayServer { MouseMode mouse_mode; Point2i center; - void _handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo = false); + void _handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); + void _handle_selection_request_event(XSelectionRequestEvent *p_event); + + String _clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const; + String _clipboard_get(Atom p_source, Window x11_window) const; //bool minimized; //bool window_has_focus; @@ -235,6 +245,7 @@ class DisplayServerX11 : public DisplayServer { void _update_real_mouse_position(const WindowData &wd); bool _window_maximize_check(WindowID p_window, const char *p_atom_name) const; + void _update_size_hints(WindowID p_window); void _set_wm_fullscreen(WindowID p_window, bool p_enabled); void _set_wm_maximized(WindowID p_window, bool p_enabled); @@ -246,6 +257,16 @@ class DisplayServerX11 : public DisplayServer { static void _dispatch_input_events(const Ref<InputEvent> &p_event); void _dispatch_input_event(const Ref<InputEvent> &p_event); + mutable Mutex events_mutex; + Thread *events_thread = nullptr; + bool events_thread_done = false; + LocalVector<XEvent> polled_events; + static void _poll_events_thread(void *ud); + void _poll_events(); + + static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg); + static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg); + protected: void _window_changed(XEvent *event); @@ -285,6 +306,8 @@ public: virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const; virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 5edaf35c50..fda1358dfd 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -311,16 +311,9 @@ void JoypadLinux::open_joypad(const char *p_path) { return; } - //check if the device supports basic gamepad events, prevents certain keyboards from - //being detected as joypads + // Check if the device supports basic gamepad events if (!(test_bit(EV_KEY, evbit) && test_bit(EV_ABS, evbit) && - (test_bit(ABS_X, absbit) || test_bit(ABS_Y, absbit) || test_bit(ABS_HAT0X, absbit) || - test_bit(ABS_GAS, absbit) || test_bit(ABS_RUDDER, absbit)) && - (test_bit(BTN_A, keybit) || test_bit(BTN_THUMBL, keybit) || - test_bit(BTN_TRIGGER, keybit) || test_bit(BTN_1, keybit))) && - !(test_bit(EV_ABS, evbit) && - test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit) && - test_bit(ABS_RX, absbit) && test_bit(ABS_RY, absbit))) { + test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit))) { close(fd); return; } diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 8c6f3b1167..e569aa03d7 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -88,7 +88,9 @@ void OS_LinuxBSD::finalize() { #endif #ifdef JOYDEV_ENABLED - memdelete(joypad); + if (joypad) { + memdelete(joypad); + } #endif } @@ -121,18 +123,39 @@ String OS_LinuxBSD::get_name() const { Error OS_LinuxBSD::shell_open(String p_uri) { Error ok; + int err_code; List<String> args; args.push_back(p_uri); - ok = execute("xdg-open", args, false); - if (ok == OK) { + + // Agnostic + ok = execute("xdg-open", args, true, nullptr, nullptr, &err_code); + if (ok == OK && !err_code) { + return OK; + } else if (err_code == 2) { + return ERR_FILE_NOT_FOUND; + } + // GNOME + args.push_front("open"); // The command is `gio open`, so we need to add it to args + ok = execute("gio", args, true, nullptr, nullptr, &err_code); + if (ok == OK && !err_code) { + return OK; + } else if (err_code == 2) { + return ERR_FILE_NOT_FOUND; + } + args.pop_front(); + ok = execute("gvfs-open", args, true, nullptr, nullptr, &err_code); + if (ok == OK && !err_code) { return OK; + } else if (err_code == 2) { + return ERR_FILE_NOT_FOUND; } - ok = execute("gnome-open", args, false); - if (ok == OK) { + // KDE + ok = execute("kde-open5", args, true, nullptr, nullptr, &err_code); + if (ok == OK && !err_code) { return OK; } - ok = execute("kde-open", args, false); - return ok; + ok = execute("kde-open", args, true, nullptr, nullptr, &err_code); + return !err_code ? ok : FAILED; } bool OS_LinuxBSD::_check_internal_feature_support(const String &p_feature) { diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 4295721c68..cd4fbd9db5 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -48,7 +48,7 @@ class OS_LinuxBSD : public OS_Unix { bool force_quit; #ifdef JOYDEV_ENABLED - JoypadLinux *joypad; + JoypadLinux *joypad = nullptr; #endif #ifdef ALSA_ENABLED diff --git a/platform/linuxbsd/platform_config.h b/platform/linuxbsd/platform_config.h index ac30519132..571ad03db0 100644 --- a/platform/linuxbsd/platform_config.h +++ b/platform/linuxbsd/platform_config.h @@ -31,9 +31,15 @@ #ifdef __linux__ #include <alloca.h> #endif -#if defined(__FreeBSD__) || defined(__OpenBSD__) -#include <stdlib.h> + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include <stdlib.h> // alloca +// FreeBSD and OpenBSD use pthread_set_name_np, while other platforms, +// include NetBSD, use pthread_setname_np. NetBSD's version however requires +// a different format, we handle this directly in thread_posix. +#ifdef __NetBSD__ +#define PTHREAD_NETBSD_SET_NAME +#else #define PTHREAD_BSD_SET_NAME #endif - -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" +#endif diff --git a/platform/osx/SCsub b/platform/osx/SCsub index ad62db358b..aa95a89444 100644 --- a/platform/osx/SCsub +++ b/platform/osx/SCsub @@ -18,5 +18,5 @@ files = [ prog = env.add_program("#bin/godot", files) -if (env["debug_symbols"] == "full" or env["debug_symbols"] == "yes") and env["separate_debug_symbols"]: +if env["debug_symbols"] == "yes" and env["separate_debug_symbols"]: env.AddPostAction(prog, run_in_subprocess(platform_osx_builders.make_debug_osx)) diff --git a/platform/osx/context_gl_osx.h b/platform/osx/context_gl_osx.h index cce00fb35f..1d467513e2 100644 --- a/platform/osx/context_gl_osx.h +++ b/platform/osx/context_gl_osx.h @@ -33,7 +33,7 @@ #if defined(OPENGL_ENABLED) || defined(GLES_ENABLED) -#include "core/error_list.h" +#include "core/error/error_list.h" #include "core/os/os.h" #include <AppKit/AppKit.h> diff --git a/platform/osx/crash_handler_osx.mm b/platform/osx/crash_handler_osx.mm index 5da0118686..1429024598 100644 --- a/platform/osx/crash_handler_osx.mm +++ b/platform/osx/crash_handler_osx.mm @@ -30,8 +30,8 @@ #include "crash_handler_osx.h" +#include "core/config/project_settings.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "main/main.h" #include <string.h> @@ -90,7 +90,7 @@ static void handle_crash(int sig) { if (OS::get_singleton()->get_main_loop()) OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { void *load_addr = (void *)load_address(); @@ -142,7 +142,7 @@ static void handle_crash(int sig) { } } - fprintf(stderr, "[%zu] %ls\n", i, output.c_str()); + fprintf(stderr, "[%zu] %s\n", i, output.utf8().get_data()); } free(strings); diff --git a/platform/osx/detect.py b/platform/osx/detect.py index 272ae1b620..64e1d94f39 100644 --- a/platform/osx/detect.py +++ b/platform/osx/detect.py @@ -1,6 +1,5 @@ import os import sys -import subprocess from methods import detect_darwin_sdk_path @@ -32,7 +31,7 @@ def get_opts(): " validation layers)", False, ), - EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), + EnumVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", "yes", ("yes", "no")), BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False), BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN))", False), @@ -58,8 +57,6 @@ def configure(env): env.Prepend(CCFLAGS=["-msse2"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "release_debug": @@ -69,13 +66,12 @@ def configure(env): env.Prepend(CCFLAGS=["-Os"]) env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "debug": env.Prepend(CCFLAGS=["-g3"]) env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) + env.Prepend(LINKFLAGS=["-Xlinker", "-no_deduplicate"]) ## Architecture @@ -139,7 +135,8 @@ def configure(env): env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define if env["CXX"] == "clang++": - env.Append(CPPDEFINES=["TYPED_METHOD_BIND"]) + # This should now work with clang++, re-enable if there are issues + # env.Append(CPPDEFINES=["TYPED_METHOD_BIND"]) env["CC"] = "clang" env["LINK"] = "clang++" diff --git a/platform/osx/dir_access_osx.h b/platform/osx/dir_access_osx.h index d61ee181f0..91b8f9b2c5 100644 --- a/platform/osx/dir_access_osx.h +++ b/platform/osx/dir_access_osx.h @@ -47,6 +47,8 @@ protected: virtual int get_drive_count(); virtual String get_drive(int p_drive); + + virtual bool is_hidden(const String &p_name); }; #endif //UNIX ENABLED diff --git a/platform/osx/dir_access_osx.mm b/platform/osx/dir_access_osx.mm index 7791ba5407..439c6a075f 100644 --- a/platform/osx/dir_access_osx.mm +++ b/platform/osx/dir_access_osx.mm @@ -68,4 +68,14 @@ String DirAccessOSX::get_drive(int p_drive) { return volname; } +bool DirAccessOSX::is_hidden(const String &p_name) { + String f = get_current_dir().plus_file(p_name); + NSURL *url = [NSURL fileURLWithPath:@(f.utf8().get_data())]; + NSNumber *hidden = nil; + if (![url getResourceValue:&hidden forKey:NSURLIsHiddenKey error:nil]) { + return DirAccessUnix::is_hidden(p_name); + } + return [hidden boolValue]; +} + #endif //posix_enabled diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index d8f3f81ff6..073d35008b 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -102,6 +102,8 @@ public: id window_object; id window_view; + Vector<Vector2> mpath; + #if defined(OPENGL_ENABLED) ContextGL_OSX *context_gles2 = nullptr; #endif @@ -240,6 +242,7 @@ public: virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window = MAIN_WINDOW_ID) override; virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index 4c04151791..1ad7117b39 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -33,6 +33,7 @@ #include "os_osx.h" #include "core/io/marshalls.h" +#include "core/math/geometry_2d.h" #include "core/os/keyboard.h" #include "main/main.h" #include "scene/resources/texture.h" @@ -45,7 +46,6 @@ #include <IOKit/hid/IOHIDLib.h> #if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" //TODO - reimplement OpenGLES #import <AppKit/NSOpenGLView.h> @@ -63,6 +63,8 @@ #define DS_OSX ((DisplayServerOSX *)(DisplayServerOSX::get_singleton())) +static bool ignore_momentum_scroll = false; + static void _get_key_modifier_state(unsigned int p_osx_state, Ref<InputEventWithModifiers> r_state) { r_state->set_shift((p_osx_state & NSEventModifierFlagShift)); r_state->set_control((p_osx_state & NSEventModifierFlagControl)); @@ -312,8 +314,6 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); } - DS_OSX->windows.erase(window_id); - if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { DisplayServerOSX::WindowData &pwd = DS_OSX->windows[wd.transient_parent]; [pwd.window_object makeKeyAndOrderFront:nil]; // Move focus back to parent. @@ -333,6 +333,8 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->context_vulkan->window_destroy(window_id); } #endif + + DS_OSX->windows.erase(window_id); } - (void)windowDidEnterFullScreen:(NSNotification *)notification { @@ -1304,6 +1306,8 @@ static int remapKey(unsigned int key, unsigned int state) { ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + ignore_momentum_scroll = true; + // Ignore all input if IME input is in progress if (!imeInputEventInProgress) { NSString *characters = [event characters]; @@ -1348,6 +1352,8 @@ static int remapKey(unsigned int key, unsigned int state) { } - (void)flagsChanged:(NSEvent *)event { + ignore_momentum_scroll = true; + // Ignore all input if IME input is in progress if (!imeInputEventInProgress) { DisplayServerOSX::KeyEvent ke; @@ -1507,6 +1513,14 @@ inline void sendPanEvent(DisplayServer::WindowID window_id, double dx, double dy deltaY *= 0.03; } + if ([event momentumPhase] != NSEventPhaseNone) { + if (ignore_momentum_scroll) { + return; + } + } else { + ignore_momentum_scroll = false; + } + if ([event phase] != NSEventPhaseNone || [event momentumPhase] != NSEventPhaseNone) { sendPanEvent(window_id, deltaX, deltaY, [event modifierFlags]); } else { @@ -2041,6 +2055,12 @@ void DisplayServerOSX::mouse_set_mode(MouseMode p_mode) { CGDisplayHideCursor(kCGDirectMainDisplay); } CGAssociateMouseAndMouseCursorPosition(false); + WindowData &wd = windows[MAIN_WINDOW_ID]; + const NSRect contentRect = [wd.window_view frame]; + NSRect pointInWindowRect = NSMakeRect(contentRect.size.width / 2, contentRect.size.height / 2, 0, 0); + NSPoint pointOnScreen = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; + CGPoint lMouseWarpPos = { pointOnScreen.x, CGDisplayBounds(CGMainDisplayID()).size.height - pointOnScreen.y }; + CGWarpMouseCursorPosition(lMouseWarpPos); } else if (p_mode == MOUSE_MODE_HIDDEN) { if (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED) { CGDisplayHideCursor(kCGDirectMainDisplay); @@ -2240,11 +2260,18 @@ int DisplayServerOSX::screen_get_dpi(int p_screen) const { NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; - NSSize displayDPI = [[description objectForKey:NSDeviceResolution] sizeValue]; - return (displayDPI.width + displayDPI.height) / 2; + + const NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + const CGSize displayPhysicalSize = CGDisplayScreenSize([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + float scale = [[screenArray objectAtIndex:p_screen] backingScaleFactor]; + + float den2 = (displayPhysicalSize.width / 25.4f) * (displayPhysicalSize.width / 25.4f) + (displayPhysicalSize.height / 25.4f) * (displayPhysicalSize.height / 25.4f); + if (den2 > 0.0f) { + return ceil(sqrt(displayPixelSize.width * displayPixelSize.width + displayPixelSize.height * displayPixelSize.height) / sqrt(den2) * scale); + } } - return 96; + return 72; } float DisplayServerOSX::screen_get_scale(int p_screen) const { @@ -2375,7 +2402,11 @@ void DisplayServerOSX::_update_window(WindowData p_wd) { [p_wd.window_object setHidesOnDeactivate:YES]; } else { // Reset these when our window is not a borderless window that covers up the screen - [p_wd.window_object setLevel:NSNormalWindowLevel]; + if (p_wd.on_top) { + [p_wd.window_object setLevel:NSFloatingWindowLevel]; + } else { + [p_wd.window_object setLevel:NSNormalWindowLevel]; + } [p_wd.window_object setHidesOnDeactivate:NO]; } } @@ -2401,6 +2432,15 @@ void DisplayServerOSX::window_set_title(const String &p_title, WindowID p_window [wd.window_object setTitle:[NSString stringWithUTF8String:p_title.utf8().get_data()]]; } +void DisplayServerOSX::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd = windows[p_window]; + + wd.mpath = p_region; +} + void DisplayServerOSX::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -3353,6 +3393,26 @@ void DisplayServerOSX::process_events() { Input::get_singleton()->flush_accumulated_events(); } + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + WindowData &wd = E->get(); + if (wd.mpath.size() > 0) { + const Vector2 mpos = _get_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); + if (Geometry2D::is_point_in_polygon(mpos, wd.mpath)) { + if ([wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:NO]; + } + } else { + if (![wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:YES]; + } + } + } else { + if ([wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:NO]; + } + } + } + [autoreleasePool drain]; autoreleasePool = [[NSAutoreleasePool alloc] init]; } @@ -3805,9 +3865,11 @@ DisplayServerOSX::~DisplayServerOSX() { } //destroy all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - [E->get().window_object setContentView:nil]; - [E->get().window_object close]; + for (Map<WindowID, WindowData>::Element *E = windows.front(); E;) { + Map<WindowID, WindowData>::Element *F = E; + E = E->next(); + [F->get().window_object setContentView:nil]; + [F->get().window_object close]; } //destroy drivers diff --git a/platform/osx/export/export.cpp b/platform/osx/export/export.cpp index ae45e0734d..e988e51e72 100644 --- a/platform/osx/export/export.cpp +++ b/platform/osx/export/export.cpp @@ -30,13 +30,13 @@ #include "export.h" +#include "core/config/project_settings.h" #include "core/io/marshalls.h" #include "core/io/resource_saver.h" #include "core/io/zip_io.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "core/version.h" #include "editor/editor_export.h" #include "editor/editor_node.h" @@ -78,7 +78,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { } for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); @@ -95,8 +95,8 @@ protected: virtual void get_export_options(List<ExportOption> *r_options) override; public: - virtual String get_name() const override { return "Mac OSX"; } - virtual String get_os_name() const override { return "OSX"; } + virtual String get_name() const override { return "macOS"; } + virtual String get_os_name() const override { return "macOS"; } virtual Ref<Texture2D> get_logo() const override { return logo; } virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { diff --git a/platform/osx/logo.png b/platform/osx/logo.png Binary files differindex 834bbf3ba6..b5a660b165 100644 --- a/platform/osx/logo.png +++ b/platform/osx/logo.png diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 9204a145bf..5a9e43450f 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -44,7 +44,7 @@ class OS_OSX : public OS_Unix { bool force_quit; - JoypadOSX *joypad_osx; + JoypadOSX *joypad_osx = nullptr; #ifdef COREAUDIO_ENABLED AudioDriverCoreAudio audio_driver; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index c4eb5407af..399a29cbe0 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -145,7 +145,9 @@ void OS_OSX::finalize() { delete_main_loop(); - memdelete(joypad_osx); + if (joypad_osx) { + memdelete(joypad_osx); + } } void OS_OSX::set_main_loop(MainLoop *p_main_loop) { diff --git a/platform/osx/platform_config.h b/platform/osx/platform_config.h index 155f37ed55..e657aca955 100644 --- a/platform/osx/platform_config.h +++ b/platform/osx/platform_config.h @@ -30,5 +30,4 @@ #include <alloca.h> -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" #define PTHREAD_RENAME_SELF diff --git a/platform/server/detect.py b/platform/server/detect.py index 4c5a4527ae..f9e151f956 100644 --- a/platform/server/detect.py +++ b/platform/server/detect.py @@ -39,7 +39,7 @@ def get_opts(): BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN))", False), BoolVariable("use_lsan", "Use LLVM/GCC compiler leak sanitizer (LSAN))", False), BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN))", False), - EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), + EnumVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", "yes", ("yes", "no")), BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), BoolVariable("execinfo", "Use libexecinfo on systems where glibc is not available", False), ] @@ -61,8 +61,6 @@ def configure(env): env.Prepend(CCFLAGS=["-Os"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "release_debug": @@ -73,8 +71,6 @@ def configure(env): env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "debug": diff --git a/platform/server/os_server.cpp b/platform/server/os_server.cpp index fbe526ef6d..9937ae5b62 100644 --- a/platform/server/os_server.cpp +++ b/platform/server/os_server.cpp @@ -30,7 +30,7 @@ #include "os_server.h" -#include "core/print_string.h" +#include "core/string/print_string.h" #include "drivers/dummy/rasterizer_dummy.h" #include "drivers/dummy/texture_loader_dummy.h" #include "servers/rendering/rendering_server_raster.h" diff --git a/platform/server/platform_config.h b/platform/server/platform_config.h index bdff93f02b..73136ec81b 100644 --- a/platform/server/platform_config.h +++ b/platform/server/platform_config.h @@ -31,10 +31,19 @@ #if defined(__linux__) || defined(__APPLE__) #include <alloca.h> #endif -#if defined(__FreeBSD__) || defined(__OpenBSD__) -#include <stdlib.h> + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include <stdlib.h> // alloca +// FreeBSD and OpenBSD use pthread_set_name_np, while other platforms, +// include NetBSD, use pthread_setname_np. NetBSD's version however requires +// a different format, we handle this directly in thread_posix. +#ifdef __NetBSD__ +#define PTHREAD_NETBSD_SET_NAME +#else #define PTHREAD_BSD_SET_NAME #endif +#endif + #ifdef __APPLE__ #define PTHREAD_RENAME_SELF #endif diff --git a/platform/uwp/context_egl_uwp.h b/platform/uwp/context_egl_uwp.h index 6f333b8e6a..5e7dc1802d 100644 --- a/platform/uwp/context_egl_uwp.h +++ b/platform/uwp/context_egl_uwp.h @@ -35,7 +35,7 @@ #include <EGL/egl.h> -#include "core/error_list.h" +#include "core/error/error_list.h" #include "core/os/os.h" using namespace Windows::UI::Core; diff --git a/platform/uwp/detect.py b/platform/uwp/detect.py index 04b743f2c8..2af7803749 100644 --- a/platform/uwp/detect.py +++ b/platform/uwp/detect.py @@ -65,12 +65,14 @@ def configure(env): env.Append(CCFLAGS=["/MD"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) elif env["target"] == "debug": env.Append(CCFLAGS=["/Zi"]) env.Append(CCFLAGS=["/MDd"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) env.Append(LINKFLAGS=["/DEBUG"]) ## Compiler configuration @@ -78,6 +80,9 @@ def configure(env): env["ENV"] = os.environ vc_base_path = os.environ["VCTOOLSINSTALLDIR"] if "VCTOOLSINSTALLDIR" in os.environ else os.environ["VCINSTALLDIR"] + # Force to use Unicode encoding + env.AppendUnique(CCFLAGS=["/utf-8"]) + # ANGLE angle_root = os.getenv("ANGLE_SRC_PATH") env.Prepend(CPPPATH=[angle_root + "/include"]) diff --git a/platform/uwp/export/export.cpp b/platform/uwp/export/export.cpp index ede0d7c76b..e0c03df48e 100644 --- a/platform/uwp/export/export.cpp +++ b/platform/uwp/export/export.cpp @@ -29,14 +29,15 @@ /*************************************************************************/ #include "export.h" -#include "core/bind/core_bind.h" + +#include "core/config/project_settings.h" +#include "core/core_bind.h" #include "core/crypto/crypto_core.h" #include "core/io/marshalls.h" #include "core/io/zip_io.h" -#include "core/object.h" +#include "core/object/class_db.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" -#include "core/project_settings.h" #include "core/version.h" #include "editor/editor_export.h" #include "editor/editor_node.h" @@ -249,7 +250,7 @@ void AppxPackager::make_content_types(const String &p_path) { Map<String, String> types; for (int i = 0; i < file_metadata.size(); i++) { - String ext = file_metadata[i].name.get_extension(); + String ext = file_metadata[i].name.get_extension().to_lower(); if (types.has(ext)) { continue; @@ -961,7 +962,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform { return true; } - static Error save_appx_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error save_appx_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { AppxPackager *packager = (AppxPackager *)p_userdata; String dst_path = p_path.replace_first("res://", "game/"); diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 1dddb07990..79508055e5 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -33,9 +33,8 @@ #include "os_uwp.h" +#include "core/config/project_settings.h" #include "core/io/marshalls.h" -#include "core/project_settings.h" -#include "drivers/gles2/rasterizer_gles2.h" #include "drivers/unix/ip_unix.h" #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" @@ -297,7 +296,7 @@ Error OS_UWP::initialize(const VideoMode &p_desired, int p_video_driver, int p_a void OS_UWP::set_clipboard(const String &p_text) { DataPackage ^ clip = ref new DataPackage(); clip->RequestedOperation = DataPackageOperation::Copy; - clip->SetText(ref new Platform::String((const wchar_t *)p_text.c_str())); + clip->SetText(ref new Platform::String((LPCWSTR)(p_text.utf16().get_data()))); Clipboard::SetContent(clip); }; @@ -347,8 +346,8 @@ void OS_UWP::finalize_core() { } void OS_UWP::alert(const String &p_alert, const String &p_title) { - Platform::String ^ alert = ref new Platform::String(p_alert.c_str()); - Platform::String ^ title = ref new Platform::String(p_title.c_str()); + Platform::String ^ alert = ref new Platform::String((LPCWSTR)(p_alert.utf16().get_data())); + Platform::String ^ title = ref new Platform::String((LPCWSTR)(p_title.utf16().get_data())); MessageDialog ^ msg = ref new MessageDialog(alert, title); @@ -739,7 +738,7 @@ static String format_error_message(DWORD id) { Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { String full_path = "game/" + p_path; - p_library_handle = (void *)LoadPackagedLibrary(full_path.c_str(), 0); + p_library_handle = (void *)LoadPackagedLibrary((LPCWSTR)(full_path.utf16().get_data()), 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + full_path + ", error: " + format_error_message(GetLastError()) + "."); return OK; } diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index 892327bac5..88961bf143 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -35,7 +35,7 @@ #include "core/input/input.h" #include "core/math/transform_2d.h" #include "core/os/os.h" -#include "core/ustring.h" +#include "core/string/ustring.h" #include "drivers/xaudio2/audio_driver_xaudio2.h" #include "joypad_uwp.h" #include "servers/audio_server.h" diff --git a/platform/windows/SCsub b/platform/windows/SCsub index daffe59f34..0c9aa77803 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -26,11 +26,11 @@ prog = env.add_program("#bin/godot", common_win + res_obj, PROGSUFFIX=env["PROGS # Microsoft Visual Studio Project Generation if env["vsproj"]: - env.vs_srcs = env.vs_srcs + ["platform/windows/" + res_file] - env.vs_srcs = env.vs_srcs + ["platform/windows/godot.natvis"] + env.vs_srcs += ["platform/windows/" + res_file] + env.vs_srcs += ["platform/windows/godot.natvis"] for x in common_win: - env.vs_srcs = env.vs_srcs + ["platform/windows/" + str(x)] + env.vs_srcs += ["platform/windows/" + str(x)] if not os.getenv("VCINSTALLDIR"): - if (env["debug_symbols"] == "full" or env["debug_symbols"] == "yes") and env["separate_debug_symbols"]: + if env["debug_symbols"] == "yes" and env["separate_debug_symbols"]: env.AddPostAction(prog, run_in_subprocess(platform_windows_builders.make_debug_mingw)) diff --git a/platform/windows/context_gl_windows.h b/platform/windows/context_gl_windows.h index 046e3437ea..0013177609 100644 --- a/platform/windows/context_gl_windows.h +++ b/platform/windows/context_gl_windows.h @@ -35,7 +35,7 @@ #ifndef CONTEXT_GL_WIN_H #define CONTEXT_GL_WIN_H -#include "core/error_list.h" +#include "core/error/error_list.h" #include "core/os/os.h" #include <windows.h> diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index 996d9722f5..7abf451062 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -30,8 +30,8 @@ #include "crash_handler_windows.h" +#include "core/config/project_settings.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "main/main.h" #ifdef CRASH_HANDLER_EXCEPTION @@ -175,7 +175,7 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { msg = proj_settings->get("debug/settings/crash_handler/message"); } - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); int n = 0; do { diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 271ffc8871..934314b2f2 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -64,7 +64,8 @@ def get_opts(): # XP support dropped after EOL due to missing API for IPv6 and other issues # Vista support dropped after EOL due to GH-10243 ("target_win_version", "Targeted Windows version, >= 0x0601 (Windows 7)", "0x0601"), - EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), + EnumVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", "yes", ("yes", "no")), + EnumVariable("windows_subsystem", "Windows subsystem", "default", ("default", "console", "gui")), BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), ("msvc_version", "MSVC version to use. Ignored if VCINSTALLDIR is set in shell env.", None), BoolVariable("use_mingw", "Use the Mingw compiler, even if MSVC is installed. Only used on Windows.", False), @@ -178,12 +179,20 @@ def configure_msvc(env, manual_msvc_config): # Build type + if env["tests"]: + env["windows_subsystem"] = "console" + elif env["windows_subsystem"] == "default": + # Default means we use console for debug, gui for release. + if "debug" in env["target"]: + env["windows_subsystem"] = "console" + else: + env["windows_subsystem"] = "gui" + if env["target"] == "release": if env["optimize"] == "speed": # optimize for speed (default) env.Append(CCFLAGS=["/O2"]) else: # optimize for size env.Append(CCFLAGS=["/O1"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:WINDOWS"]) env.Append(LINKFLAGS=["/ENTRY:mainCRTStartup"]) env.Append(LINKFLAGS=["/OPT:REF"]) @@ -193,24 +202,28 @@ def configure_msvc(env, manual_msvc_config): else: # optimize for size env.Append(CCFLAGS=["/O1"]) env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.Append(LINKFLAGS=["/OPT:REF"]) elif env["target"] == "debug": env.AppendUnique(CCFLAGS=["/Z7", "/Od", "/EHsc"]) env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.Append(LINKFLAGS=["/DEBUG"]) - if env["debug_symbols"] == "full" or env["debug_symbols"] == "yes": + if env["debug_symbols"] == "yes": env.AppendUnique(CCFLAGS=["/Z7"]) env.AppendUnique(LINKFLAGS=["/DEBUG"]) + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["/SUBSYSTEM:WINDOWS"]) + else: + env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + ## Compile/link flags env.AppendUnique(CCFLAGS=["/MT", "/Gd", "/GR", "/nologo"]) - if int(env["MSVC_VERSION"].split(".")[0]) >= 14: # vs2015 and later - env.AppendUnique(CCFLAGS=["/utf-8"]) + # Force to use Unicode encoding + env.AppendUnique(CCFLAGS=["/utf-8"]) env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++ if manual_msvc_config: # should be automatic if SCons found it if os.getenv("WindowsSdkDir") is not None: @@ -303,6 +316,15 @@ def configure_mingw(env): ## Build type + if env["tests"]: + env["windows_subsystem"] = "console" + elif env["windows_subsystem"] == "default": + # Default means we use console for debug, gui for release. + if "debug" in env["target"]: + env["windows_subsystem"] = "console" + else: + env["windows_subsystem"] = "gui" + if env["target"] == "release": env.Append(CCFLAGS=["-msse2"]) @@ -314,19 +336,13 @@ def configure_mingw(env): else: # optimize for size env.Prepend(CCFLAGS=["-Os"]) - env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) - if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) elif env["target"] == "release_debug": env.Append(CCFLAGS=["-O2"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) if env["debug_symbols"] == "yes": - env.Prepend(CCFLAGS=["-g1"]) - if env["debug_symbols"] == "full": env.Prepend(CCFLAGS=["-g2"]) if env["optimize"] == "speed": # optimize for speed (default) env.Append(CCFLAGS=["-O2"]) @@ -337,6 +353,12 @@ def configure_mingw(env): env.Append(CCFLAGS=["-g3"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) + else: + env.Append(LINKFLAGS=["-Wl,--subsystem,console"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + ## Compiler configuration if os.name != "nt": @@ -426,7 +448,7 @@ def configure_mingw(env): else: env.Append(LIBS=["cfgmgr32"]) - ## TODO !!! Reenable when OpenGLES Rendering Device is implemented !!! + ## TODO !!! Re-enable when OpenGLES Rendering Device is implemented !!! # env.Append(CPPDEFINES=['OPENGL_ENABLED']) env.Append(LIBS=["opengl32"]) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index da2fc1c2c1..dfbb734ee4 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -29,7 +29,9 @@ /*************************************************************************/ #include "display_server_windows.h" + #include "core/io/marshalls.h" +#include "core/math/geometry_2d.h" #include "main/main.h" #include "os_windows.h" #include "scene/resources/texture.h" @@ -42,7 +44,7 @@ static String format_error_message(DWORD id) { size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr); - String msg = "Error " + itos(id) + ": " + String(messageBuffer, size); + String msg = "Error " + itos(id) + ": " + String::utf16((const char16_t *)messageBuffer, size); LocalFree(messageBuffer); @@ -78,7 +80,7 @@ String DisplayServerWindows::get_name() const { } void DisplayServerWindows::alert(const String &p_alert, const String &p_title) { - MessageBoxW(nullptr, p_alert.c_str(), p_title.c_str(), MB_OK | MB_ICONEXCLAMATION | MB_TASKMODAL); + MessageBoxW(nullptr, (LPCWSTR)(p_alert.utf16().get_data()), (LPCWSTR)(p_title.utf16().get_data()), MB_OK | MB_ICONEXCLAMATION | MB_TASKMODAL); } void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { @@ -177,11 +179,12 @@ void DisplayServerWindows::clipboard_set(const String &p_text) { } EmptyClipboard(); - HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (text.length() + 1) * sizeof(CharType)); + Char16String utf16 = text.utf16(); + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (utf16.length() + 1) * sizeof(WCHAR)); ERR_FAIL_COND_MSG(mem == nullptr, "Unable to allocate memory for clipboard contents."); LPWSTR lptstrCopy = (LPWSTR)GlobalLock(mem); - memcpy(lptstrCopy, text.c_str(), (text.length() + 1) * sizeof(CharType)); + memcpy(lptstrCopy, utf16.get_data(), (utf16.length() + 1) * sizeof(WCHAR)); GlobalUnlock(mem); SetClipboardData(CF_UNICODETEXT, mem); @@ -218,7 +221,7 @@ String DisplayServerWindows::clipboard_get() const { if (mem != nullptr) { LPWSTR ptr = (LPWSTR)GlobalLock(mem); if (ptr != nullptr) { - ret = String((CharType *)ptr); + ret = String::utf16((const char16_t *)ptr); GlobalUnlock(mem); }; }; @@ -493,14 +496,16 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod wd.no_focus = true; } - _update_window_style(window_id); - return window_id; } void DisplayServerWindows::show_window(WindowID p_id) { WindowData &wd = windows[p_id]; + if (p_id != MAIN_WINDOW_ID) { + _update_window_style(p_id); + } + ShowWindow(wd.hWnd, wd.no_focus ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window if (!wd.no_focus) { SetForegroundWindow(wd.hWnd); // Slightly Higher Priority @@ -591,7 +596,37 @@ void DisplayServerWindows::window_set_title(const String &p_title, WindowID p_wi _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - SetWindowTextW(windows[p_window].hWnd, p_title.c_str()); + SetWindowTextW(windows[p_window].hWnd, (LPCWSTR)(p_title.utf16().get_data())); +} + +void DisplayServerWindows::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + windows[p_window].mpath = p_region; + _update_window_mouse_passthrough(p_window); +} + +void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) { + if (windows[p_window].mpath.size() == 0) { + SetWindowRgn(windows[p_window].hWnd, nullptr, TRUE); + } else { + POINT *points = (POINT *)memalloc(sizeof(POINT) * windows[p_window].mpath.size()); + for (int i = 0; i < windows[p_window].mpath.size(); i++) { + if (windows[p_window].borderless) { + points[i].x = windows[p_window].mpath[i].x; + points[i].y = windows[p_window].mpath[i].y; + } else { + points[i].x = windows[p_window].mpath[i].x + GetSystemMetrics(SM_CXSIZEFRAME); + points[i].y = windows[p_window].mpath[i].y + GetSystemMetrics(SM_CYSIZEFRAME) + GetSystemMetrics(SM_CYCAPTION); + } + } + + HRGN region = CreatePolygonRgn(points, windows[p_window].mpath.size(), ALTERNATE); + SetWindowRgn(windows[p_window].hWnd, region, TRUE); + DeleteObject(region); + memfree(points); + } } int DisplayServerWindows::window_get_current_screen(WindowID p_window) const { @@ -1007,6 +1042,7 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W case WINDOW_FLAG_BORDERLESS: { wd.borderless = p_enabled; _update_window_style(p_window); + _update_window_mouse_passthrough(p_window); } break; case WINDOW_FLAG_ALWAYS_ON_TOP: { ERR_FAIL_COND_MSG(wd.transient_parent != INVALID_WINDOW_ID && p_enabled, "Transient windows can't become on top"); @@ -1421,13 +1457,13 @@ String DisplayServerWindows::keyboard_get_layout_language(int p_index) const { HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL)); GetKeyboardLayoutList(layout_count, layouts); - wchar_t buf[LOCALE_NAME_MAX_LENGTH]; - memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t)); + WCHAR buf[LOCALE_NAME_MAX_LENGTH]; + memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)); LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0); memfree(layouts); - return String(buf).substr(0, 2); + return String::utf16((const char16_t *)buf).substr(0, 2); } String _get_full_layout_name_from_registry(HKL p_layout) { @@ -1435,17 +1471,17 @@ String _get_full_layout_name_from_registry(HKL p_layout) { String ret; HKEY hkey; - wchar_t layout_text[1024]; - memset(layout_text, 0, 1024 * sizeof(wchar_t)); + WCHAR layout_text[1024]; + memset(layout_text, 0, 1024 * sizeof(WCHAR)); - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)id.c_str(), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) { + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)(id.utf16().get_data()), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) { return ret; } DWORD buffer = 1024; DWORD vtype = REG_SZ; if (RegQueryValueExW(hkey, L"Layout Text", NULL, &vtype, (LPBYTE)layout_text, &buffer) == ERROR_SUCCESS) { - ret = String(layout_text); + ret = String::utf16((const char16_t *)layout_text); } RegCloseKey(hkey); return ret; @@ -1461,15 +1497,15 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const { String ret = _get_full_layout_name_from_registry(layouts[p_index]); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine). if (ret == String()) { - wchar_t buf[LOCALE_NAME_MAX_LENGTH]; - memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t)); + WCHAR buf[LOCALE_NAME_MAX_LENGTH]; + memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)); LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0); - wchar_t name[1024]; - memset(name, 0, 1024 * sizeof(wchar_t)); + WCHAR name[1024]; + memset(name, 0, 1024 * sizeof(WCHAR)); GetLocaleInfoEx(buf, LOCALE_SLOCALIZEDDISPLAYNAME, (LPWSTR)&name, 1024); - ret = String(name); + ret = String::utf16((const char16_t *)name); } memfree(layouts); @@ -2030,8 +2066,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Ref<InputEventMouseMotion> mm; mm.instance(); mm->set_window_id(window_id); - mm->set_control(GetKeyState(VK_CONTROL) != 0); - mm->set_shift(GetKeyState(VK_SHIFT) != 0); + mm->set_control(GetKeyState(VK_CONTROL) < 0); + mm->set_shift(GetKeyState(VK_SHIFT) < 0); mm->set_alt(alt_mem); mm->set_pressure(windows[window_id].last_pressure); @@ -2173,8 +2209,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_tilt(Vector2((float)pen_info.tiltX / 90, (float)pen_info.tiltY / 90)); } - mm->set_control((wParam & MK_CONTROL) != 0); - mm->set_shift((wParam & MK_SHIFT) != 0); + mm->set_control(GetKeyState(VK_CONTROL) < 0); + mm->set_shift(GetKeyState(VK_SHIFT) < 0); mm->set_alt(alt_mem); mm->set_button_mask(last_button_state); @@ -2709,7 +2745,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_DROPFILES: { HDROP hDropInfo = (HDROP)wParam; const int buffsize = 4096; - wchar_t buf[buffsize]; + WCHAR buf[buffsize]; int fcount = DragQueryFileW(hDropInfo, 0xFFFFFFFF, nullptr, 0); @@ -2717,7 +2753,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA for (int i = 0; i < fcount; i++) { DragQueryFileW(hDropInfo, i, buf, buffsize); - String file = buf; + String file = String::utf16((const char16_t *)buf); files.push_back(file); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 7bd93a7086..c64a1b3b09 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -33,9 +33,9 @@ #include "servers/display_server.h" +#include "core/config/project_settings.h" #include "core/input/input.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "crash_handler_windows.h" #include "drivers/unix/ip_unix.h" #include "drivers/wasapi/audio_driver_wasapi.h" @@ -323,6 +323,8 @@ private: HWND hWnd; //layered window + Vector<Vector2> mpath; + bool preserve_window_size = false; bool pre_fs_valid = false; RECT pre_fs_rect; @@ -416,6 +418,7 @@ private: void _touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx); void _update_window_style(WindowID p_window, bool p_repaint = true, bool p_maximized = false); + void _update_window_mouse_passthrough(WindowID p_window); void _update_real_mouse_position(WindowID p_window); @@ -477,6 +480,7 @@ public: virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window = MAIN_WINDOW_ID); virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const; virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID); diff --git a/platform/windows/godot.natvis b/platform/windows/godot.natvis index 593557cc69..1f625cfb77 100644 --- a/platform/windows/godot.natvis +++ b/platform/windows/godot.natvis @@ -19,7 +19,7 @@ </ArrayItems> </Expand> </Type> - + <Type Name="List<*>"> <Expand> <Item Name="[size]">_data ? (_data->size_cache) : 0</Item> @@ -49,7 +49,7 @@ <DisplayString Condition="type == Variant::QUAT">{*(Quat *)_data._mem}</DisplayString> <DisplayString Condition="type == Variant::COLOR">{*(Color *)_data._mem}</DisplayString> <DisplayString Condition="type == Variant::NODE_PATH">{*(NodePath *)_data._mem}</DisplayString> - <DisplayString Condition="type == Variant::_RID">{*(RID *)_data._mem}</DisplayString> + <DisplayString Condition="type == Variant::RID">{*(RID *)_data._mem}</DisplayString> <DisplayString Condition="type == Variant::OBJECT">{*(Object *)_data._mem}</DisplayString> <DisplayString Condition="type == Variant::DICTIONARY">{*(Dictionary *)_data._mem}</DisplayString> <DisplayString Condition="type == Variant::ARRAY">{*(Array *)_data._mem}</DisplayString> @@ -62,7 +62,7 @@ <DisplayString Condition="type == Variant::POOL_COLOR_ARRAY">{*(PoolColorArray *)_data._mem}</DisplayString> <StringView Condition="type == Variant::STRING && ((String *)(_data._mem))->_cowdata._ptr">((String *)(_data._mem))->_cowdata._ptr,su</StringView> - + <Expand> <Item Name="[value]" Condition="type == Variant::BOOL">_data._bool</Item> <Item Name="[value]" Condition="type == Variant::INT">_data._int</Item> @@ -79,7 +79,7 @@ <Item Name="[value]" Condition="type == Variant::QUAT">*(Quat *)_data._mem</Item> <Item Name="[value]" Condition="type == Variant::COLOR">*(Color *)_data._mem</Item> <Item Name="[value]" Condition="type == Variant::NODE_PATH">*(NodePath *)_data._mem</Item> - <Item Name="[value]" Condition="type == Variant::_RID">*(RID *)_data._mem</Item> + <Item Name="[value]" Condition="type == Variant::RID">*(RID *)_data._mem</Item> <Item Name="[value]" Condition="type == Variant::OBJECT">*(Object *)_data._mem</Item> <Item Name="[value]" Condition="type == Variant::DICTIONARY">*(Dictionary *)_data._mem</Item> <Item Name="[value]" Condition="type == Variant::ARRAY">*(Array *)_data._mem</Item> @@ -143,7 +143,7 @@ <Item Name="alpha">a</Item> </Expand> </Type> - + <Type Name="Node" Inheritable="false"> <Expand> <Item Name="Object">(Object*)this</Item> diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 65caee3035..2a5c8a7763 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -146,8 +146,8 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { if (have_device(instance->guidInstance) || num == -1) return false; - d_joypads[joypad_count] = dinput_gamepad(); - dinput_gamepad *joy = &d_joypads[joypad_count]; + d_joypads[num] = dinput_gamepad(); + dinput_gamepad *joy = &d_joypads[num]; const DWORD devtype = (instance->dwDevType & 0xFF); @@ -171,7 +171,7 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { WORD version = 0; sprintf_s(uid, "%04x%04x%04x%04x%04x%04x%04x%04x", type, 0, vendor, 0, product, 0, version, 0); - id_to_change = joypad_count; + id_to_change = num; slider_count = 0; joy->di_joy->SetDataFormat(&c_dfDIJoystick2); @@ -194,7 +194,7 @@ void JoypadWindows::setup_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_ HRESULT res; DIPROPRANGE prop_range; DIPROPDWORD dilong; - DWORD ofs; + LONG ofs; if (ob->guidType == GUID_XAxis) ofs = DIJOFS_X; else if (ob->guidType == GUID_YAxis) @@ -395,7 +395,7 @@ void JoypadWindows::process_joypads() { // on mingw, these constants are not constants int count = 8; - unsigned int axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, DIJOFS_SLIDER(0), DIJOFS_SLIDER(1) }; + LONG axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, (LONG)DIJOFS_SLIDER(0), (LONG)DIJOFS_SLIDER(1) }; int values[] = { js.lX, js.lY, js.lZ, js.lRx, js.lRy, js.lRz, js.rglSlider[0], js.rglSlider[1] }; for (int j = 0; j < joy->joy_axis.size(); j++) { diff --git a/platform/windows/joypad_windows.h b/platform/windows/joypad_windows.h index c961abf0a5..223b44fcd6 100644 --- a/platform/windows/joypad_windows.h +++ b/platform/windows/joypad_windows.h @@ -77,7 +77,7 @@ private: DWORD last_pad; LPDIRECTINPUTDEVICE8 di_joy; - List<DWORD> joy_axis; + List<LONG> joy_axis; GUID guid; dinput_gamepad() { diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 5b15896b0c..b108d74b2e 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -84,7 +84,7 @@ static String format_error_message(DWORD id) { size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr); - String msg = "Error " + itos(id) + ": " + String(messageBuffer, size); + String msg = "Error " + itos(id) + ": " + String::utf16((const char16_t *)messageBuffer, size); LocalFree(messageBuffer); @@ -107,15 +107,11 @@ void RedirectIOToConsole() { // set the screen buffer to be big enough to let us scroll text - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), - - &coninfo); + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); coninfo.dwSize.Y = MAX_CONSOLE_LINES; - SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), - - coninfo.dwSize); + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); // redirect unbuffered STDOUT to the console @@ -248,7 +244,7 @@ void OS_Windows::finalize_core() { } Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { - String path = p_path; + String path = p_path.replace("/", "\\"); if (!FileAccess::exists(path)) { //this code exists so gdnative can load .dll files from within the executable path @@ -265,10 +261,10 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han DLL_DIRECTORY_COOKIE cookie = nullptr; if (p_also_set_library_path && has_dll_directory_api) { - cookie = add_dll_directory(path.get_base_dir().c_str()); + cookie = add_dll_directory((LPCWSTR)(path.get_base_dir().utf16().get_data())); } - p_library_handle = (void *)LoadLibraryExW(path.c_str(), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); + p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + format_error_message(GetLastError()) + "."); if (cookie) { @@ -407,7 +403,7 @@ uint64_t OS_Windows::get_ticks_usec() const { String OS_Windows::_quote_command_line_argument(const String &p_text) const { for (int i = 0; i < p_text.size(); i++) { - CharType c = p_text[i]; + char32_t c = p_text[i]; if (c == ' ' || c == '&' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '=' || c == ';' || c == '!' || c == '\'' || c == '+' || c == ',' || c == '`' || c == '~') { return "\"" + p_text + "\""; } @@ -416,8 +412,10 @@ String OS_Windows::_quote_command_line_argument(const String &p_text) const { } Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) { + String path = p_path.replace("/", "\\"); + if (p_blocking && r_pipe) { - String argss = _quote_command_line_argument(p_path); + String argss = _quote_command_line_argument(path); for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) { argss += " " + _quote_command_line_argument(E->get()); } @@ -428,7 +426,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, // Note: _wpopen is calling command as "cmd.exe /c argss", instead of executing it directly, add extra quotes around full command, to prevent it from stripping quotes in the command. argss = _quote_command_line_argument(argss); - FILE *f = _wpopen(argss.c_str(), L"r"); + FILE *f = _wpopen((LPCWSTR)(argss.utf16().get_data()), L"r"); ERR_FAIL_COND_V(!f, ERR_CANT_OPEN); char buf[65535]; @@ -450,7 +448,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, return OK; } - String cmdline = _quote_command_line_argument(p_path); + String cmdline = _quote_command_line_argument(path); const List<String>::Element *I = p_arguments.front(); while (I) { cmdline += " " + _quote_command_line_argument(I->get()); @@ -463,13 +461,8 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, ZeroMemory(&pi.pi, sizeof(pi.pi)); LPSTARTUPINFOW si_w = (LPSTARTUPINFOW)&pi.si; - Vector<CharType> modstr; // Windows wants to change this no idea why. - modstr.resize(cmdline.size()); - for (int i = 0; i < cmdline.size(); i++) { - modstr.write[i] = cmdline[i]; - } - - int ret = CreateProcessW(nullptr, modstr.ptrw(), nullptr, nullptr, 0, NORMAL_PRIORITY_CLASS & CREATE_NO_WINDOW, nullptr, nullptr, si_w, &pi.pi); + Char16String modstr = cmdline.utf16(); // Windows wants to change this no idea why. + int ret = CreateProcessW(nullptr, (LPWSTR)(modstr.ptrw()), nullptr, nullptr, 0, NORMAL_PRIORITY_CLASS & CREATE_NO_WINDOW, nullptr, nullptr, si_w, &pi.pi); ERR_FAIL_COND_V(ret == 0, ERR_CANT_FORK); if (p_blocking) { @@ -509,26 +502,26 @@ int OS_Windows::get_process_id() const { } Error OS_Windows::set_cwd(const String &p_cwd) { - if (_wchdir(p_cwd.c_str()) != 0) + if (_wchdir((LPCWSTR)(p_cwd.utf16().get_data())) != 0) return ERR_CANT_OPEN; return OK; } String OS_Windows::get_executable_path() const { - wchar_t bufname[4096]; + WCHAR bufname[4096]; GetModuleFileNameW(nullptr, bufname, 4096); - String s = bufname; + String s = String::utf16((const char16_t *)bufname).replace("\\", "/"); return s; } bool OS_Windows::has_environment(const String &p_var) const { #ifdef MINGW_ENABLED - return _wgetenv(p_var.c_str()) != nullptr; + return _wgetenv((LPCWSTR)(p_var.utf16().get_data())) != nullptr; #else - wchar_t *env; + WCHAR *env; size_t len; - _wdupenv_s(&env, &len, p_var.c_str()); + _wdupenv_s(&env, &len, (LPCWSTR)(p_var.utf16().get_data())); const bool has_env = env != nullptr; free(env); return has_env; @@ -536,16 +529,16 @@ bool OS_Windows::has_environment(const String &p_var) const { }; String OS_Windows::get_environment(const String &p_var) const { - wchar_t wval[0x7Fff]; // MSDN says 32767 char is the maximum - int wlen = GetEnvironmentVariableW(p_var.c_str(), wval, 0x7Fff); + WCHAR wval[0x7fff]; // MSDN says 32767 char is the maximum + int wlen = GetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), wval, 0x7fff); if (wlen > 0) { - return wval; + return String::utf16((const char16_t *)wval); } return ""; } bool OS_Windows::set_environment(const String &p_var, const String &p_value) const { - return (bool)SetEnvironmentVariableW(p_var.c_str(), p_value.c_str()); + return (bool)SetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), (LPCWSTR)(p_value.utf16().get_data())); } String OS_Windows::get_stdin_string(bool p_block) { @@ -558,7 +551,7 @@ String OS_Windows::get_stdin_string(bool p_block) { } Error OS_Windows::shell_open(String p_uri) { - ShellExecuteW(nullptr, nullptr, p_uri.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + ShellExecuteW(nullptr, nullptr, (LPCWSTR)(p_uri.utf16().get_data()), nullptr, nullptr, SW_SHOWNORMAL); return OK; } @@ -701,7 +694,7 @@ String OS_Windows::get_system_dir(SystemDir p_dir) const { PWSTR szPath; HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &szPath); ERR_FAIL_COND_V(res != S_OK, String()); - String path = String(szPath); + String path = String::utf16((const char16_t *)szPath); CoTaskMemFree(szPath); return path; } @@ -727,7 +720,7 @@ String OS_Windows::get_user_data_dir() const { String OS_Windows::get_unique_id() const { HW_PROFILE_INFO HwProfInfo; ERR_FAIL_COND_V(!GetCurrentHwProfile(&HwProfInfo), ""); - return String(HwProfInfo.szHwProfileGuid); + return String::utf16((const char16_t *)(HwProfInfo.szHwProfileGuid), HW_PROFILE_GUIDLEN); } bool OS_Windows::_check_internal_feature_support(const String &p_feature) { @@ -744,9 +737,11 @@ bool OS_Windows::is_disable_crash_handler() const { Error OS_Windows::move_to_trash(const String &p_path) { SHFILEOPSTRUCTW sf; - WCHAR *from = new WCHAR[p_path.length() + 2]; - wcscpy_s(from, p_path.length() + 1, p_path.c_str()); - from[p_path.length() + 1] = 0; + + Char16String utf16 = p_path.utf16(); + WCHAR *from = new WCHAR[utf16.length() + 2]; + wcscpy_s(from, utf16.length() + 1, (LPCWSTR)(utf16.get_data())); + from[utf16.length() + 1] = 0; sf.hwnd = main_window; sf.wFunc = FO_DELETE; diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 910a83539a..a3dbb23182 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -31,9 +31,9 @@ #ifndef OS_WINDOWS_H #define OS_WINDOWS_H +#include "core/config/project_settings.h" #include "core/input/input.h" #include "core/os/os.h" -#include "core/project_settings.h" #include "crash_handler_windows.h" #include "drivers/unix/ip_unix.h" #include "drivers/wasapi/audio_driver_wasapi.h" diff --git a/platform/windows/platform_config.h b/platform/windows/platform_config.h index 290decac5f..09a16614e0 100644 --- a/platform/windows/platform_config.h +++ b/platform/windows/platform_config.h @@ -29,5 +29,3 @@ /*************************************************************************/ #include <malloc.h> - -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" |