Implement cursor visibility and quit key shortcuts

Fixes #1255
This commit is contained in:
Cameron Gutman 2023-09-23 02:20:26 -04:00
parent 978a879c43
commit 081cca48fb
6 changed files with 140 additions and 79 deletions

View File

@ -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;
}
else {
// Toggle the grab if Z comes up
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
}
// 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;
}
// Toggle the grab if control or shift comes up
else if (grabComboDown) {
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 {
// 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;
grabComboDown = false;
// 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;
}
// Reset special key state
specialKeyCode = KeyEvent.KEYCODE_UNKNOWN;
waitingForAllModifiersUp = 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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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