From 46f091a803f4ef8c5d96a1e12c392c6f2898ed93 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Wed, 5 Oct 2022 10:43:36 -0700 Subject: [PATCH] Add cursor shape support for the Android platform --- platform/android/SCsub | 1 + .../org/godotengine/editor/GodotEditor.kt | 2 +- .../lib/src/org/godotengine/godot/Godot.java | 5 ++ .../src/org/godotengine/godot/GodotView.java | 26 ++++++++ platform/android/java_godot_view_wrapper.cpp | 66 +++++++++++++++++++ platform/android/java_godot_view_wrapper.h | 55 ++++++++++++++++ platform/android/java_godot_wrapper.cpp | 27 +++++++- platform/android/java_godot_wrapper.h | 5 ++ platform/android/os_android.cpp | 40 +++++++++-- platform/android/os_android.h | 33 +++++++++- 10 files changed, 248 insertions(+), 12 deletions(-) create mode 100644 platform/android/java_godot_view_wrapper.cpp create mode 100644 platform/android/java_godot_view_wrapper.h diff --git a/platform/android/SCsub b/platform/android/SCsub index bcf3b4e2ce5..23d7f966734 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -16,6 +16,7 @@ android_files = [ "java_class_wrapper.cpp", "java_godot_wrapper.cpp", "java_godot_io_wrapper.cpp", + "java_godot_view_wrapper.cpp", "jni_utils.cpp", "android_keys_utils.cpp", "plugin/godot_plugin_jni.cpp", diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 5623c41e2b9..489a81fc1a7 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -79,7 +79,7 @@ open class GodotEditor : FullScreenGodotApp() { super.onCreate(savedInstanceState) // Enable long press, panning and scaling gestures - godotFragment?.mView?.inputHandler?.apply { + godotFragment?.renderView?.inputHandler?.apply { enableLongPress(enableLongPressGestures()) enablePanningAndScalingGestures(enablePanAndScaleGestures()) } 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 e680e0433ba..a312a85b774 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -568,6 +568,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return mView.getHolder().getSurface(); } + @Keep + public GodotView getRenderView() { // used by native side to get renderView + return mView; + } + /** * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. * @return The input fallback mapping for the current XR mode. diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java index 700c67a6cd8..de5a86a397a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java @@ -44,8 +44,12 @@ import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; +import android.os.Build; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.PointerIcon; + +import androidx.annotation.Keep; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; @@ -89,6 +93,10 @@ public class GodotView extends GLSurfaceView { this.inputHandler = new GodotInputHandler(this); this.godotRenderer = new GodotRenderer(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); + } + init(xrMode, p_translucent); } @@ -118,6 +126,24 @@ public class GodotView extends GLSurfaceView { return inputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); } + /** + * Called from JNI to change the pointer icon + */ + @Keep + private void setPointerIcon(int pointerType) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); + } + } + + @Override + public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPointerIcon(); + } + return super.onResolvePointerIcon(event, pointerIndex); + } + private void init(XRMode xrMode, boolean translucent) { setPreserveEGLContextOnPause(true); setFocusableInTouchMode(true); diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp new file mode 100644 index 00000000000..ad647f87474 --- /dev/null +++ b/platform/android/java_godot_view_wrapper.cpp @@ -0,0 +1,66 @@ +/*************************************************************************/ +/* java_godot_view_wrapper.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +#include "java_godot_view_wrapper.h" + +GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + _godot_view = env->NewGlobalRef(godot_view); + + _cls = (jclass)env->NewGlobalRef(env->GetObjectClass(godot_view)); + + int android_device_api_level = android_get_device_api_level(); + if (android_device_api_level >= __ANDROID_API_N__) { + _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); + } +} + +bool GodotJavaViewWrapper::can_update_pointer_icon() const { + return _set_pointer_icon != nullptr; +} + +void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) { + if (_set_pointer_icon != nullptr) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->CallVoidMethod(_godot_view, _set_pointer_icon, pointer_type); + } +} + +GodotJavaViewWrapper::~GodotJavaViewWrapper() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(_godot_view); + env->DeleteGlobalRef(_cls); +} diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h new file mode 100644 index 00000000000..5c7e15735f3 --- /dev/null +++ b/platform/android/java_godot_view_wrapper.h @@ -0,0 +1,55 @@ +/*************************************************************************/ +/* java_godot_view_wrapper.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 JAVA_GODOT_VIEW_WRAPPER_H +#define JAVA_GODOT_VIEW_WRAPPER_H + +#include +#include + +#include "string_android.h" + +// Class that makes functions in java/src/org/godotengine/godot/GodotView.java callable from C++ +class GodotJavaViewWrapper { +private: + jclass _cls; + jobject _godot_view; + jmethodID _set_pointer_icon = 0; + +public: + GodotJavaViewWrapper(jobject godot_view); + + bool can_update_pointer_icon() const; + void set_pointer_icon(int pointer_type); + + ~GodotJavaViewWrapper(); +}; + +#endif // JAVA_GODOT_VIEW_WRAPPER_H diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index c100784a28c..8a1787b1a13 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -81,13 +81,23 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V"); + _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotView;"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); } GodotJavaWrapper::~GodotJavaWrapper() { - // nothing to do here for now + if (godot_view) { + delete godot_view; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + env->DeleteGlobalRef(godot_instance); + env->DeleteGlobalRef(godot_class); + env->DeleteGlobalRef(activity); + env->DeleteGlobalRef(activity_class); } jobject GodotJavaWrapper::get_activity() { @@ -117,6 +127,21 @@ jobject GodotJavaWrapper::get_class_loader() { } } +GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { + if (godot_view != nullptr) { + return godot_view; + } + if (_get_render_view) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, nullptr); + jobject godot_render_view = env->CallObjectMethod(godot_instance, _get_render_view); + if (!env->IsSameObject(godot_render_view, nullptr)) { + godot_view = new GodotJavaViewWrapper(godot_render_view); + } + } + return godot_view; +} + void GodotJavaWrapper::on_video_init(JNIEnv *p_env) { if (_on_video_init) { if (p_env == nullptr) { diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index f664973ab3e..09d87f3a518 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -38,6 +38,7 @@ #include #include "core/list.h" +#include "java_godot_view_wrapper.h" #include "string_android.h" // Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++ @@ -48,6 +49,8 @@ private: jclass godot_class; jclass activity_class; + GodotJavaViewWrapper *godot_view = nullptr; + jmethodID _on_video_init = nullptr; jmethodID _create_offscreen_gl = nullptr; jmethodID _destroy_offscreen_gl = nullptr; @@ -72,6 +75,7 @@ private: jmethodID _on_godot_main_loop_started = nullptr; jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; + jmethodID _get_render_view = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -81,6 +85,7 @@ public: jobject get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env = NULL); jobject get_class_loader(); + GodotJavaViewWrapper *get_godot_view(); bool create_offscreen_gl(JNIEnv *p_env); void destroy_offscreen_gl(JNIEnv *p_env); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 71601278294..3aaec945e95 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -273,17 +273,43 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han return OK; } -void OS_Android::set_mouse_show(bool p_show) { - //android has no mouse... +void OS_Android::set_mouse_mode(MouseMode p_mode) { + if (!godot_java->get_godot_view()->can_update_pointer_icon()) { + return; + } + if (mouse_mode == p_mode || p_mode == MouseMode::MOUSE_MODE_CAPTURED) { + return; + } + + if (p_mode == MouseMode::MOUSE_MODE_HIDDEN) { + godot_java->get_godot_view()->set_pointer_icon(CURSOR_TYPE_NULL); + } else { + set_cursor_shape(cursor_shape); + } + + mouse_mode = p_mode; } -void OS_Android::set_mouse_grab(bool p_grab) { - //it really has no mouse...! +OS::MouseMode OS_Android::get_mouse_mode() const { + return mouse_mode; } -bool OS_Android::is_mouse_grab_enabled() const { - //*sigh* technology has evolved so much since i was a kid.. - return false; +void OS_Android::set_cursor_shape(CursorShape p_shape) { + if (!godot_java->get_godot_view()->can_update_pointer_icon()) { + return; + } + if (cursor_shape == p_shape) { + return; + } + + cursor_shape = p_shape; + if (mouse_mode == MouseMode::MOUSE_MODE_VISIBLE || mouse_mode == MouseMode::MOUSE_MODE_CONFINED) { + godot_java->get_godot_view()->set_pointer_icon(android_cursors[cursor_shape]); + } +} + +OS::CursorShape OS_Android::get_cursor_shape() const { + return cursor_shape; } Point2 OS_Android::get_mouse_position() const { diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 09121d5a9fe..3bc3dce0a59 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -42,6 +42,31 @@ class GodotJavaWrapper; class GodotIOJavaWrapper; class OS_Android : public OS_Unix { + // https://developer.android.com/reference/android/view/PointerIcon + // mapping between Godot's cursor shape to Android's' + int android_cursors[CURSOR_MAX] = { + 1000, //CURSOR_ARROW + 1008, //CURSOR_IBEAM + 1002, //CURSOR_POINTIN + 1007, //CURSOR_CROSS + 1004, //CURSOR_WAIT + 1004, //CURSOR_BUSY + 1021, //CURSOR_DRAG + 1021, //CURSOR_CAN_DRO + 1000, //CURSOR_FORBIDD (no corresponding icon in Android's icon so fallback to default) + 1015, //CURSOR_VSIZE + 1014, //CURSOR_HSIZE + 1017, //CURSOR_BDIAGSI + 1016, //CURSOR_FDIAGSI + 1020, //CURSOR_MOVE + 1015, //CURSOR_VSPLIT + 1014, //CURSOR_HSPLIT + 1003, //CURSOR_HELP + }; + const int CURSOR_TYPE_NULL = 0; + MouseMode mouse_mode = MouseMode::MOUSE_MODE_VISIBLE; + CursorShape cursor_shape = CursorShape::CURSOR_ARROW; + bool use_apk_expansion; bool secondary_gl_available = false; @@ -106,9 +131,6 @@ public: virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); - virtual void set_mouse_show(bool p_show); - virtual void set_mouse_grab(bool p_grab); - virtual bool is_mouse_grab_enabled() const; virtual Point2 get_mouse_position() const; virtual int get_mouse_button_state() const; virtual void set_window_title(const String &p_title); @@ -141,6 +163,11 @@ public: virtual void hide_virtual_keyboard(); virtual int get_virtual_keyboard_height() const; + virtual void set_cursor_shape(CursorShape p_shape); + virtual CursorShape get_cursor_shape() const; + virtual void set_mouse_mode(MouseMode p_mode); + virtual MouseMode get_mouse_mode() const; + void set_opengl_extensions(const char *p_gl_extensions); void set_display_size(Size2 p_size);