From 081cca48fb1e69e79bbc222457a9bd05c0627f07 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 23 Sep 2023 02:20:26 -0400 Subject: [PATCH] Implement cursor visibility and quit key shortcuts Fixes #1255 --- app/src/main/java/com/limelight/Game.java | 90 +++++++++++++------ .../AndroidNativePointerCaptureProvider.java | 28 +++--- .../AndroidPointerIconCaptureProvider.java | 13 ++- .../input/capture/InputCaptureProvider.java | 11 +++ .../input/capture/ShieldCaptureProvider.java | 10 +-- .../input/evdev/EvdevCaptureProvider.java | 67 ++++++++------ 6 files changed, 140 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 90e5a299..c4195efa 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -132,7 +132,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, private InputCaptureProvider inputCaptureProvider; private int modifierFlags = 0; private boolean grabbedInput = true; - private boolean grabComboDown = false; + private boolean cursorVisible = false; + private boolean waitingForAllModifiersUp = false; + private int specialKeyCode = KeyEvent.KEYCODE_UNKNOWN; private StreamView streamView; private long lastAbsTouchUpTime = 0; private long lastAbsTouchDownTime = 0; @@ -1188,6 +1190,12 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Grab/ungrab the mouse cursor if (grab) { inputCaptureProvider.enableCapture(); + + // Enabling capture may hide the cursor again, so + // we will need to show it again. + if (cursorVisible) { + inputCaptureProvider.showCursor(); + } } else { inputCaptureProvider.disableCapture(); @@ -1209,6 +1217,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Returns true if the key stroke was consumed private boolean handleSpecialKeys(int androidKeyCode, boolean down) { int modifierMask = 0; + int nonModifierKeyCode = KeyEvent.KEYCODE_UNKNOWN; if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT || androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) { @@ -1226,6 +1235,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, androidKeyCode == KeyEvent.KEYCODE_META_RIGHT) { modifierMask = KeyboardPacket.MODIFIER_META; } + else { + nonModifierKeyCode = androidKeyCode; + } if (down) { this.modifierFlags |= modifierMask; @@ -1234,36 +1246,62 @@ public class Game extends Activity implements SurfaceHolder.Callback, this.modifierFlags &= ~modifierMask; } - // Check if Ctrl+Alt+Shift+Z is pressed - if (androidKeyCode == KeyEvent.KEYCODE_Z && - (modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) == - (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) - { - if (down) { - // Now that we've pressed the magic combo - // we'll wait for one of the keys to come up - grabComboDown = true; + // Handle the special combos on the key up + if (waitingForAllModifiersUp || specialKeyCode != KeyEvent.KEYCODE_UNKNOWN) { + if (specialKeyCode == androidKeyCode) { + // If this is a key up for the special key itself, eat that because the host never saw the original key down + return true; + } + else if (modifierFlags != 0) { + // While we're waiting for modifiers to come up, eat all key downs and allow all key ups to pass + return down; } else { - // Toggle the grab if Z comes up - Handler h = getWindow().getDecorView().getHandler(); - if (h != null) { - h.postDelayed(toggleGrab, 250); + // When all modifiers are up, perform the special action + switch (specialKeyCode) { + // Toggle input grab + case KeyEvent.KEYCODE_Z: + Handler h = getWindow().getDecorView().getHandler(); + if (h != null) { + h.postDelayed(toggleGrab, 250); + } + break; + + // Quit + case KeyEvent.KEYCODE_Q: + finish(); + break; + + // Toggle cursor visibility + case KeyEvent.KEYCODE_C: + if (!grabbedInput) { + inputCaptureProvider.enableCapture(); + grabbedInput = true; + } + cursorVisible = !cursorVisible; + if (cursorVisible) { + inputCaptureProvider.showCursor(); + } else { + inputCaptureProvider.hideCursor(); + } + break; + + default: + break; } - grabComboDown = false; + // Reset special key state + specialKeyCode = KeyEvent.KEYCODE_UNKNOWN; + waitingForAllModifiersUp = false; } - - return true; } - // Toggle the grab if control or shift comes up - else if (grabComboDown) { - Handler h = getWindow().getDecorView().getHandler(); - if (h != null) { - h.postDelayed(toggleGrab, 250); - } - - grabComboDown = false; + // Check if Ctrl+Alt+Shift is down when a non-modifier key is pressed + else if ((modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) == + (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT) && + (down && nonModifierKeyCode != KeyEvent.KEYCODE_UNKNOWN)) { + // Remember that a special key combo was activated, so we can consume all key events until the modifiers come up + specialKeyCode = androidKeyCode; + waitingForAllModifiersUp = true; return true; } @@ -1762,7 +1800,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Returns true if the event was consumed // NB: View is only present if called from a view callback private boolean handleMotionEvent(View view, MotionEvent event) { - // Pass through keyboard input if we're not grabbing + // Pass through mouse/touch/joystick input if we're not grabbing if (!grabbedInput) { return false; } diff --git a/app/src/main/java/com/limelight/binding/input/capture/AndroidNativePointerCaptureProvider.java b/app/src/main/java/com/limelight/binding/input/capture/AndroidNativePointerCaptureProvider.java index b104784c..3109dcc2 100644 --- a/app/src/main/java/com/limelight/binding/input/capture/AndroidNativePointerCaptureProvider.java +++ b/app/src/main/java/com/limelight/binding/input/capture/AndroidNativePointerCaptureProvider.java @@ -15,8 +15,8 @@ import android.view.View; // is unavailable on this system (ex: DeX, ChromeOS) @TargetApi(Build.VERSION_CODES.O) public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider implements InputManager.InputDeviceListener { - private InputManager inputManager; - private View targetView; + private final InputManager inputManager; + private final View targetView; public AndroidNativePointerCaptureProvider(Activity activity, View targetView) { super(activity, targetView); @@ -62,8 +62,16 @@ public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptu } @Override - public void enableCapture() { - super.enableCapture(); + public void showCursor() { + super.showCursor(); + + inputManager.unregisterInputDeviceListener(this); + targetView.releasePointerCapture(); + } + + @Override + public void hideCursor() { + super.hideCursor(); // Listen for device events to enable/disable capture inputManager.registerInputDeviceListener(this, null); @@ -74,16 +82,12 @@ public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptu } } - @Override - public void disableCapture() { - super.disableCapture(); - inputManager.unregisterInputDeviceListener(this); - targetView.releasePointerCapture(); - } - @Override public void onWindowFocusChanged(boolean focusActive) { - if (!focusActive || !isCapturing) { + // NB: We have to check cursor visibility here because Android pointer capture + // doesn't support capturing the cursor while it's visible. Enabling pointer + // capture implicitly hides the cursor. + if (!focusActive || !isCapturing || isCursorVisible) { return; } diff --git a/app/src/main/java/com/limelight/binding/input/capture/AndroidPointerIconCaptureProvider.java b/app/src/main/java/com/limelight/binding/input/capture/AndroidPointerIconCaptureProvider.java index fa3d3f30..6a2d472a 100644 --- a/app/src/main/java/com/limelight/binding/input/capture/AndroidPointerIconCaptureProvider.java +++ b/app/src/main/java/com/limelight/binding/input/capture/AndroidPointerIconCaptureProvider.java @@ -4,14 +4,13 @@ import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.os.Build; -import android.view.MotionEvent; import android.view.PointerIcon; import android.view.View; @TargetApi(Build.VERSION_CODES.N) public class AndroidPointerIconCaptureProvider extends InputCaptureProvider { - private View targetView; - private Context context; + private final View targetView; + private final Context context; public AndroidPointerIconCaptureProvider(Activity activity, View targetView) { this.context = activity; @@ -23,14 +22,14 @@ public class AndroidPointerIconCaptureProvider extends InputCaptureProvider { } @Override - public void enableCapture() { - super.enableCapture(); + public void hideCursor() { + super.hideCursor(); targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL)); } @Override - public void disableCapture() { - super.disableCapture(); + public void showCursor() { + super.showCursor(); targetView.setPointerIcon(null); } } diff --git a/app/src/main/java/com/limelight/binding/input/capture/InputCaptureProvider.java b/app/src/main/java/com/limelight/binding/input/capture/InputCaptureProvider.java index b4e16f42..3070be5e 100644 --- a/app/src/main/java/com/limelight/binding/input/capture/InputCaptureProvider.java +++ b/app/src/main/java/com/limelight/binding/input/capture/InputCaptureProvider.java @@ -4,12 +4,15 @@ import android.view.MotionEvent; public abstract class InputCaptureProvider { protected boolean isCapturing; + protected boolean isCursorVisible; public void enableCapture() { isCapturing = true; + hideCursor(); } public void disableCapture() { isCapturing = false; + showCursor(); } public void destroy() {} @@ -22,6 +25,14 @@ public abstract class InputCaptureProvider { return isCapturing; } + public void showCursor() { + isCursorVisible = true; + } + + public void hideCursor() { + isCursorVisible = false; + } + public boolean eventHasRelativeMouseAxes(MotionEvent event) { return false; } diff --git a/app/src/main/java/com/limelight/binding/input/capture/ShieldCaptureProvider.java b/app/src/main/java/com/limelight/binding/input/capture/ShieldCaptureProvider.java index 2237a490..b7f7a86d 100644 --- a/app/src/main/java/com/limelight/binding/input/capture/ShieldCaptureProvider.java +++ b/app/src/main/java/com/limelight/binding/input/capture/ShieldCaptureProvider.java @@ -22,7 +22,7 @@ public class ShieldCaptureProvider extends InputCaptureProvider { private static int AXIS_RELATIVE_X; private static int AXIS_RELATIVE_Y; - private Context context; + private final Context context; static { try { @@ -62,14 +62,14 @@ public class ShieldCaptureProvider extends InputCaptureProvider { } @Override - public void enableCapture() { - super.enableCapture(); + public void hideCursor() { + super.hideCursor(); setCursorVisibility(false); } @Override - public void disableCapture() { - super.disableCapture(); + public void showCursor() { + super.showCursor(); setCursorVisibility(true); } diff --git a/app/src/root/java/com.limelight/binding/input/evdev/EvdevCaptureProvider.java b/app/src/root/java/com.limelight/binding/input/evdev/EvdevCaptureProvider.java index 83c09312..ab436f01 100644 --- a/app/src/root/java/com.limelight/binding/input/evdev/EvdevCaptureProvider.java +++ b/app/src/root/java/com.limelight/binding/input/evdev/EvdevCaptureProvider.java @@ -110,6 +110,9 @@ public class EvdevCaptureProvider extends InputCaptureProvider { break; } + // Note: The EvdevReader process already filters input events when grabbing + // is not enabled, so we don't need to that here. + switch (event.type) { case EvdevEvent.EV_SYN: if (deltaX != 0 || deltaY != 0) { @@ -231,35 +234,8 @@ public class EvdevCaptureProvider extends InputCaptureProvider { } @Override - public void enableCapture() { - super.enableCapture(); - if (!started) { - // Start the handler thread if it's our first time - // capturing - handlerThread.start(); - started = true; - } - else { - // This may be called on the main thread - runInNetworkSafeContextSynchronously(new Runnable() { - @Override - public void run() { - // Send a request to regrab if we're already capturing - if (!shutdown && evdevOut != null) { - try { - evdevOut.write(REGRAB_REQUEST); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - }); - } - } - - @Override - public void disableCapture() { - super.disableCapture(); + public void showCursor() { + super.showCursor(); // This may be called on the main thread runInNetworkSafeContextSynchronously(new Runnable() { @Override @@ -275,6 +251,39 @@ public class EvdevCaptureProvider extends InputCaptureProvider { }); } + @Override + public void hideCursor() { + super.hideCursor(); + // This may be called on the main thread + runInNetworkSafeContextSynchronously(new Runnable() { + @Override + public void run() { + // Send a request to regrab if we're already capturing + if (started && !shutdown && evdevOut != null) { + try { + evdevOut.write(REGRAB_REQUEST); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + }); + } + + @Override + public void enableCapture() { + if (!started) { + // Start the handler thread if it's our first time + // capturing + handlerThread.start(); + started = true; + } + + // Call the superclass only after we've started the handler thread. + // It will invoke hideCursor() when we call it. + super.enableCapture(); + } + @Override public void destroy() { // We need to stop the process in this context otherwise