From 7ab3510d4dbda3f2c3c9e764000e23ea9d0901e2 Mon Sep 17 00:00:00 2001 From: artdeell Date: Fri, 19 Jan 2024 11:04:14 +0300 Subject: [PATCH] Feat[touch_input]: unified gesture system, rclick/lclick/hotbar drop gestures reimplemented --- .../kdt/pojavlaunch/MinecraftGLSurface.java | 4 +- .../customcontrols/mouse/DropGesture.java | 28 +++++ .../mouse/InGUIEventProcessor.java | 6 +- ...ocessor.java => InGameEventProcessor.java} | 117 +++++------------- .../mouse/LeftClickGesture.java | 55 ++++++++ .../mouse/RightClickGesture.java | 46 +++++++ .../mouse/ValidatorGesture.java | 71 +++++++++++ 7 files changed, 238 insertions(+), 89 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/DropGesture.java rename app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/{IngameEventProcessor.java => InGameEventProcessor.java} (50%) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java index bcbdcef49..e1cc45225 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java @@ -28,7 +28,7 @@ import androidx.annotation.RequiresApi; import net.kdt.pojavlaunch.customcontrols.ControlLayout; import net.kdt.pojavlaunch.customcontrols.gamepad.Gamepad; import net.kdt.pojavlaunch.customcontrols.mouse.InGUIEventProcessor; -import net.kdt.pojavlaunch.customcontrols.mouse.IngameEventProcessor; +import net.kdt.pojavlaunch.customcontrols.mouse.InGameEventProcessor; import net.kdt.pojavlaunch.customcontrols.mouse.TouchEventProcessor; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.utils.JREUtils; @@ -75,7 +75,7 @@ public class MinecraftGLSurface extends View implements GrabListener { /* View holding the surface, either a SurfaceView or a TextureView */ View mSurface; - private final TouchEventProcessor mIngameProcessor = new IngameEventProcessor(mScaleFactor, mSensitivityFactor); + private final TouchEventProcessor mIngameProcessor = new InGameEventProcessor(mScaleFactor, mSensitivityFactor); private final TouchEventProcessor mInGUIProcessor = new InGUIEventProcessor(mScaleFactor); private boolean mLastGrabState = false; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/DropGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/DropGesture.java new file mode 100644 index 000000000..bfb8d677a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/DropGesture.java @@ -0,0 +1,28 @@ +package net.kdt.pojavlaunch.customcontrols.mouse; + +import static org.lwjgl.glfw.CallbackBridge.sendKeyPress; + +import android.os.Handler; + +import net.kdt.pojavlaunch.LwjglGlfwKeycode; + +public class DropGesture extends ValidatorGesture { + public DropGesture(Handler mHandler) { + super(mHandler, 250); + } + boolean mGuiBarHit; + + public void submit(boolean hasGuiBarHit) { + submit(); + mGuiBarHit = hasGuiBarHit; + } + + @Override + public boolean checkAndTrigger() { + if(mGuiBarHit) sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_Q); + return true; + } + + @Override + public void onGestureCancelled(boolean isSwitching) {} +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGUIEventProcessor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGUIEventProcessor.java index 0579af1ae..6f64b913a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGUIEventProcessor.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGUIEventProcessor.java @@ -21,7 +21,7 @@ public class InGUIEventProcessor implements TouchEventProcessor { switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: mTracker.startTracking(motionEvent); - sendTouchCoords(motionEvent.getX(), motionEvent.getY()); + sendTouchCoordinates(motionEvent.getX(), motionEvent.getY()); enableMouse(); break; case MotionEvent.ACTION_MOVE: @@ -30,7 +30,7 @@ public class InGUIEventProcessor implements TouchEventProcessor { float mainPointerX = motionEvent.getX(pointerIndex); float mainPointerY = motionEvent.getY(pointerIndex); if(pointerCount == 1 || LauncherPreferences.PREF_DISABLE_GESTURES) { - sendTouchCoords(mainPointerX, mainPointerY); + sendTouchCoordinates(mainPointerX, mainPointerY); if(!mIsMouseDown) enableMouse(); }else { float[] motionVector = mTracker.getMotionVector(); @@ -45,7 +45,7 @@ public class InGUIEventProcessor implements TouchEventProcessor { return true; } - private void sendTouchCoords(float x, float y) { + private void sendTouchCoordinates(float x, float y) { CallbackBridge.mouseX = x * mScaleFactor; CallbackBridge.mouseY = y * mScaleFactor; CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/IngameEventProcessor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java similarity index 50% rename from app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/IngameEventProcessor.java rename to app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java index e79eff166..358fd1d13 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/IngameEventProcessor.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java @@ -2,50 +2,37 @@ package net.kdt.pojavlaunch.customcontrols.mouse; import static net.kdt.pojavlaunch.utils.MCOptionUtils.getMcScale; -import static org.lwjgl.glfw.CallbackBridge.sendKeyPress; -import static org.lwjgl.glfw.CallbackBridge.sendMouseButton; - import android.os.Handler; import android.os.Looper; -import android.os.Message; import android.view.MotionEvent; -import androidx.annotation.NonNull; - import net.kdt.pojavlaunch.LwjglGlfwKeycode; -import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.TapDetector; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.utils.MCOptionUtils; -import net.kdt.pojavlaunch.utils.MathUtils; import org.lwjgl.glfw.CallbackBridge; -public class IngameEventProcessor implements TouchEventProcessor { +public class InGameEventProcessor implements TouchEventProcessor { private int mGuiScale; @SuppressWarnings("FieldCanBeLocal") // it can't, otherwise the weak reference will disappear private final MCOptionUtils.MCOptionListener mGuiScaleListener = () -> mGuiScale = getMcScale(); - public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9); - public static final int MSG_LEFT_MOUSE_BUTTON_CHECK = 1028; - public static final int MSG_DROP_ITEM_BUTTON_CHECK = 1029; - private final Handler mGestureHandler = new Handler(Looper.getMainLooper()) { - public void handleMessage(@NonNull Message msg) {IngameEventProcessor.this.handleMessage(msg);} - }; - + private final Handler mGestureHandler = new Handler(Looper.getMainLooper()); private static final int[] HOTBAR_KEYS = { LwjglGlfwKeycode.GLFW_KEY_1, LwjglGlfwKeycode.GLFW_KEY_2, LwjglGlfwKeycode.GLFW_KEY_3, LwjglGlfwKeycode.GLFW_KEY_4, LwjglGlfwKeycode.GLFW_KEY_5, LwjglGlfwKeycode.GLFW_KEY_6, LwjglGlfwKeycode.GLFW_KEY_7, LwjglGlfwKeycode.GLFW_KEY_8, LwjglGlfwKeycode.GLFW_KEY_9}; - private float mGestureStartX, mGestureStartY; - private boolean mHasPendingLongpressGesture; - private boolean mHasPendingDropGesture; - private boolean mGestureTriggered; private int mLastHudKey; private final float mScaleFactor; private final double mSensitivity; private final PointerTracker mTracker = new PointerTracker(); + private final LeftClickGesture mLeftClickGesture = new LeftClickGesture(mGestureHandler); + private final RightClickGesture mRightClickGesture = new RightClickGesture(mGestureHandler); + private final DropGesture mDropGesture = new DropGesture(mGestureHandler); + private final TapDetector mDoubleTapDetector = new TapDetector(2, TapDetector.DETECTION_METHOD_DOWN); - public IngameEventProcessor(float scaleFactor, double sensitivity) { + public InGameEventProcessor(float scaleFactor, double sensitivity) { MCOptionUtils.addMCOptionListener(mGuiScaleListener); mScaleFactor = scaleFactor; mSensitivity = sensitivity; @@ -53,9 +40,12 @@ public class IngameEventProcessor implements TouchEventProcessor { @Override public boolean processTouchEvent(MotionEvent motionEvent) { + boolean hasDoubleTapped = mDoubleTapDetector.onTouchEvent(motionEvent); switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: mTracker.startTracking(motionEvent); + if(LauncherPreferences.PREF_DISABLE_GESTURES) break; + checkGestures(handleGuiBar(motionEvent), hasDoubleTapped); break; case MotionEvent.ACTION_MOVE: mTracker.trackEvent(motionEvent); @@ -65,24 +55,35 @@ public class IngameEventProcessor implements TouchEventProcessor { CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); boolean hasGuiBarHit = handleGuiBar(motionEvent); if(LauncherPreferences.PREF_DISABLE_GESTURES) break; - checkLongpressGesture(); - checkGuiBarGesture(hasGuiBarHit); + checkGestures(hasGuiBarHit, hasDoubleTapped); break; case MotionEvent.ACTION_UP: - cancelGestures(); + case MotionEvent.ACTION_CANCEL: + cancelGestures(false); } return true; } @Override public void cancelPendingActions() { - cancelGestures(); + cancelGestures(true); } - private void cancelGestures() { - cancelLongpressGesture(); - cancelDropGesture(); - if(mGestureTriggered) sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, false); + private void checkGestures(boolean hasGuiBarHit, boolean hasDoubleTapped) { + if(!hasGuiBarHit) { + mLeftClickGesture.inputEvent(); + mRightClickGesture.inputEvent(); + } + mDropGesture.submit(hasGuiBarHit); + if(hasGuiBarHit && hasDoubleTapped && !LauncherPreferences.PREF_DISABLE_SWAP_HAND) { + CallbackBridge.sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_F); + } + } + + private void cancelGestures(boolean isSwitching) { + mLeftClickGesture.cancel(isSwitching); + mRightClickGesture.cancel(isSwitching); + mDropGesture.cancel(isSwitching); } private boolean handleGuiBar(MotionEvent motionEvent) { @@ -101,51 +102,15 @@ public class IngameEventProcessor implements TouchEventProcessor { return hasGuiBarHit; } - private void checkGuiBarGesture(boolean hasGuiBarHit) { - if(hasGuiBarHit && !mHasPendingDropGesture) submitDropGesture(); - if(!hasGuiBarHit) cancelDropGesture(); - } - - private void checkLongpressGesture() { - if(mHasPendingLongpressGesture && - MathUtils.dist(CallbackBridge.mouseX, CallbackBridge.mouseY, mGestureStartX, mGestureStartY) - >= FINGER_STILL_THRESHOLD) { - cancelLongpressGesture(); - } - if(!mHasPendingLongpressGesture) submitLongpressGesture(); - } - - private void cancelLongpressGesture() { - mGestureHandler.removeMessages(MSG_LEFT_MOUSE_BUTTON_CHECK); - mHasPendingLongpressGesture = false; - } - - private void submitLongpressGesture() { - mGestureStartX = CallbackBridge.mouseX; - mGestureStartY = CallbackBridge.mouseY; - mGestureHandler.sendEmptyMessageDelayed(MSG_LEFT_MOUSE_BUTTON_CHECK, LauncherPreferences.PREF_LONGPRESS_TRIGGER); - mHasPendingLongpressGesture = true; - } - - private void cancelDropGesture() { - mGestureHandler.removeMessages(MSG_DROP_ITEM_BUTTON_CHECK); - mHasPendingDropGesture = false; - } - - private void submitDropGesture() { - mGestureHandler.sendEmptyMessageDelayed(MSG_DROP_ITEM_BUTTON_CHECK, 350); - mHasPendingDropGesture = true; - } - /** @return the hotbar key, given the position. -1 if no key are pressed */ public int handleGuiBar(int x, int y) { if (!CallbackBridge.isGrabbing()) return -1; - int barHeight = mcscale(20); + int barHeight = mcScale(20); int barY = CallbackBridge.physicalHeight - barHeight; if(y < barY) return -1; - int barWidth = mcscale(180); + int barWidth = mcScale(180); int barX = (CallbackBridge.physicalWidth / 2) - (barWidth / 2); if(x < barX || x >= barX + barWidth) return -1; @@ -153,23 +118,7 @@ public class IngameEventProcessor implements TouchEventProcessor { } /** Return the size, given the UI scale size */ - private int mcscale(int input) { + private int mcScale(int input) { return (int)((mGuiScale * input)/ mScaleFactor); } - - private void handleMessage(Message message) { - switch (message.what) { - case MSG_LEFT_MOUSE_BUTTON_CHECK: - float x = CallbackBridge.mouseX; - float y = CallbackBridge.mouseY; - if (MathUtils.dist(x, y, mGestureStartX, mGestureStartY) < FINGER_STILL_THRESHOLD) { - sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true); - mGestureTriggered = true; - } - return; - case MSG_DROP_ITEM_BUTTON_CHECK: - sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_Q); - mGestureHandler.sendEmptyMessageDelayed(MSG_DROP_ITEM_BUTTON_CHECK, 600); - } - } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java new file mode 100644 index 000000000..9b2ac5d57 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java @@ -0,0 +1,55 @@ +package net.kdt.pojavlaunch.customcontrols.mouse; + +import static org.lwjgl.glfw.CallbackBridge.sendMouseButton; + +import android.os.Handler; + +import net.kdt.pojavlaunch.LwjglGlfwKeycode; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.utils.MathUtils; + +import org.lwjgl.glfw.CallbackBridge; + +public class LeftClickGesture extends ValidatorGesture { + public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9); + private float mGestureStartX, mGestureStartY; + + public LeftClickGesture(Handler handler) { + super(handler, LauncherPreferences.PREF_LONGPRESS_TRIGGER); + } + + public final void inputEvent() { + if(submit()) { + mGestureStartX = CallbackBridge.mouseX; + mGestureStartY = CallbackBridge.mouseY; + } + } + + @Override + public boolean checkAndTrigger() { + boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY); + if(fingerStill) sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true); + return fingerStill; + } + + @Override + public void onGestureCancelled(boolean isSwitching) { + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, false); + } + + /** + * Check if the finger is still when compared to mouseX/mouseY in CallbackBridge. + * @param startX the starting X of the gesture + * @param startY the starting Y of the gesture + * @return whether the finger's position counts as "still" or not + */ + public static boolean isFingerStill(float startX, float startY) { + return MathUtils.dist( + CallbackBridge.mouseX, + CallbackBridge.mouseY, + startX, + startY + ) <= FINGER_STILL_THRESHOLD; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java new file mode 100644 index 000000000..f00b598b7 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java @@ -0,0 +1,46 @@ +package net.kdt.pojavlaunch.customcontrols.mouse; + +import android.os.Handler; + +import net.kdt.pojavlaunch.LwjglGlfwKeycode; + +import org.lwjgl.glfw.CallbackBridge; + +public class RightClickGesture extends ValidatorGesture{ + private boolean mGestureEnabled = true; + private boolean mGestureValid = true; + private float mGestureStartX, mGestureStartY; + public RightClickGesture(Handler mHandler) { + super(mHandler, 150); + } + + public final void inputEvent() { + if(!mGestureEnabled) return; + if(submit()) { + mGestureStartX = CallbackBridge.mouseX; + mGestureStartY = CallbackBridge.mouseY; + mGestureEnabled = false; + mGestureValid = true; + } + } + + @Override + public boolean checkAndTrigger() { + // If the validate() method was called, it means that the user held on for too long. The cancellation should be ignored. + mGestureValid = false; + // Never call onGestureCancelled. This way we will be able to reserve that only for when + // the gesture is stopped in the code (when the user lets go of the screen or the tap was + // cancelled by turning on the grab) + return true; + } + + @Override + public void onGestureCancelled(boolean isSwitching) { + mGestureEnabled = true; + if(!mGestureValid || isSwitching) return; + boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY); + if(!fingerStill) return; + CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true); + CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java new file mode 100644 index 000000000..6dc6c993b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java @@ -0,0 +1,71 @@ +package net.kdt.pojavlaunch.customcontrols.mouse; + +import android.os.Handler; + +/** + * This class implements an abstract "validator gesture", meant as a base for implementation of + * more complex gestures with pinger position tracking and such. + */ +public abstract class ValidatorGesture implements Runnable{ + private final Handler mHandler; + private boolean mGestureActive; + private final int mRequiredDuration; + + /** + * @param mHandler the Handler that will be used for calling back the checkAndTrigger() method. + * This Handler should run on the same thread as the callee of submit()/cancel() + * @param mRequiredDuration the duration after which the class will call checkAndTrigger(). + */ + public ValidatorGesture(Handler mHandler, int mRequiredDuration) { + this.mHandler = mHandler; + this.mRequiredDuration = mRequiredDuration; + } + + /** + * Submit the gesture, starting the timer and marking this gesture as "active". + * If the gesture was already active, this call will be ignored + * @return true if the gesture was submitted, false if the call was ignored + */ + public final boolean submit() { + if(mGestureActive) return false; + mHandler.postDelayed(this, mRequiredDuration); + mGestureActive = true; + return true; + } + + /** + * Cancel the gesture, stopping the timer and marking this gesture as "inactive". + * If the gesture was already inactive, this call will be ignored. + * @param isSwitching true if this gesture was cancelled due to user interaction (the user let go of the finger) + * false if this gesture is cancelled due a request from the programmer or the OS. + * Note that returning false from checkAndTrigger() counts as user interaction. + */ + public final void cancel(boolean isSwitching) { + if(!mGestureActive) return; + mHandler.removeCallbacks(this); + onGestureCancelled(isSwitching); + mGestureActive = false; + } + + @Override + public final void run() { + if(checkAndTrigger()) return; + mGestureActive = false; + onGestureCancelled(false); + } + + /** + * This method will be called after mRequiredDuration milliseconds, if the gesture was not cancelled. + * @return false if you want to mark this gesture as "inactive" + * true otherwise + */ + public abstract boolean checkAndTrigger(); + + /** + * This method will be called if the gesture was cancelled using the cancel() method or by returning false + * from checkAndTrigger(). + * @param isSwitching true if this gesture was cancelled due to user interaction (the user let go of the finger) + * false if this gesture is cancelled due a request from the programmer or the OS. + */ + public abstract void onGestureCancelled(boolean isSwitching); +}