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 InputCaptureProvider inputCaptureProvider;
private int modifierFlags = 0; private int modifierFlags = 0;
private boolean grabbedInput = true; 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 StreamView streamView;
private long lastAbsTouchUpTime = 0; private long lastAbsTouchUpTime = 0;
private long lastAbsTouchDownTime = 0; private long lastAbsTouchDownTime = 0;
@ -1188,6 +1190,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Grab/ungrab the mouse cursor // Grab/ungrab the mouse cursor
if (grab) { if (grab) {
inputCaptureProvider.enableCapture(); inputCaptureProvider.enableCapture();
// Enabling capture may hide the cursor again, so
// we will need to show it again.
if (cursorVisible) {
inputCaptureProvider.showCursor();
}
} }
else { else {
inputCaptureProvider.disableCapture(); inputCaptureProvider.disableCapture();
@ -1209,6 +1217,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Returns true if the key stroke was consumed // Returns true if the key stroke was consumed
private boolean handleSpecialKeys(int androidKeyCode, boolean down) { private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
int modifierMask = 0; int modifierMask = 0;
int nonModifierKeyCode = KeyEvent.KEYCODE_UNKNOWN;
if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT || if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) { androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
@ -1226,6 +1235,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
androidKeyCode == KeyEvent.KEYCODE_META_RIGHT) { androidKeyCode == KeyEvent.KEYCODE_META_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_META; modifierMask = KeyboardPacket.MODIFIER_META;
} }
else {
nonModifierKeyCode = androidKeyCode;
}
if (down) { if (down) {
this.modifierFlags |= modifierMask; this.modifierFlags |= modifierMask;
@ -1234,36 +1246,62 @@ public class Game extends Activity implements SurfaceHolder.Callback,
this.modifierFlags &= ~modifierMask; this.modifierFlags &= ~modifierMask;
} }
// Check if Ctrl+Alt+Shift+Z is pressed // Handle the special combos on the key up
if (androidKeyCode == KeyEvent.KEYCODE_Z && if (waitingForAllModifiersUp || specialKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) == if (specialKeyCode == androidKeyCode) {
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) // If this is a key up for the special key itself, eat that because the host never saw the original key down
{ return true;
if (down) { }
// Now that we've pressed the magic combo else if (modifierFlags != 0) {
// we'll wait for one of the keys to come up // While we're waiting for modifiers to come up, eat all key downs and allow all key ups to pass
grabComboDown = true; return down;
} }
else { else {
// Toggle the grab if Z comes up // When all modifiers are up, perform the special action
Handler h = getWindow().getDecorView().getHandler(); switch (specialKeyCode) {
if (h != null) { // Toggle input grab
h.postDelayed(toggleGrab, 250); 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 // Check if Ctrl+Alt+Shift is down when a non-modifier key is pressed
else if (grabComboDown) { else if ((modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT)) ==
Handler h = getWindow().getDecorView().getHandler(); (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_ALT | KeyboardPacket.MODIFIER_SHIFT) &&
if (h != null) { (down && nonModifierKeyCode != KeyEvent.KEYCODE_UNKNOWN)) {
h.postDelayed(toggleGrab, 250); // Remember that a special key combo was activated, so we can consume all key events until the modifiers come up
} specialKeyCode = androidKeyCode;
waitingForAllModifiersUp = true;
grabComboDown = false;
return true; return true;
} }
@ -1762,7 +1800,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Returns true if the event was consumed // Returns true if the event was consumed
// NB: View is only present if called from a view callback // NB: View is only present if called from a view callback
private boolean handleMotionEvent(View view, MotionEvent event) { 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) { if (!grabbedInput) {
return false; return false;
} }

View File

@ -15,8 +15,8 @@ import android.view.View;
// is unavailable on this system (ex: DeX, ChromeOS) // is unavailable on this system (ex: DeX, ChromeOS)
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider implements InputManager.InputDeviceListener { public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider implements InputManager.InputDeviceListener {
private InputManager inputManager; private final InputManager inputManager;
private View targetView; private final View targetView;
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) { public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
super(activity, targetView); super(activity, targetView);
@ -62,8 +62,16 @@ public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptu
} }
@Override @Override
public void enableCapture() { public void showCursor() {
super.enableCapture(); super.showCursor();
inputManager.unregisterInputDeviceListener(this);
targetView.releasePointerCapture();
}
@Override
public void hideCursor() {
super.hideCursor();
// Listen for device events to enable/disable capture // Listen for device events to enable/disable capture
inputManager.registerInputDeviceListener(this, null); 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 @Override
public void onWindowFocusChanged(boolean focusActive) { 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; return;
} }

View File

@ -4,14 +4,13 @@ import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.view.MotionEvent;
import android.view.PointerIcon; import android.view.PointerIcon;
import android.view.View; import android.view.View;
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider { public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private View targetView; private final View targetView;
private Context context; private final Context context;
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) { public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
this.context = activity; this.context = activity;
@ -23,14 +22,14 @@ public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
} }
@Override @Override
public void enableCapture() { public void hideCursor() {
super.enableCapture(); super.hideCursor();
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL)); targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
} }
@Override @Override
public void disableCapture() { public void showCursor() {
super.disableCapture(); super.showCursor();
targetView.setPointerIcon(null); targetView.setPointerIcon(null);
} }
} }

View File

@ -4,12 +4,15 @@ import android.view.MotionEvent;
public abstract class InputCaptureProvider { public abstract class InputCaptureProvider {
protected boolean isCapturing; protected boolean isCapturing;
protected boolean isCursorVisible;
public void enableCapture() { public void enableCapture() {
isCapturing = true; isCapturing = true;
hideCursor();
} }
public void disableCapture() { public void disableCapture() {
isCapturing = false; isCapturing = false;
showCursor();
} }
public void destroy() {} public void destroy() {}
@ -22,6 +25,14 @@ public abstract class InputCaptureProvider {
return isCapturing; return isCapturing;
} }
public void showCursor() {
isCursorVisible = true;
}
public void hideCursor() {
isCursorVisible = false;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) { public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false; return false;
} }

View File

@ -22,7 +22,7 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
private static int AXIS_RELATIVE_X; private static int AXIS_RELATIVE_X;
private static int AXIS_RELATIVE_Y; private static int AXIS_RELATIVE_Y;
private Context context; private final Context context;
static { static {
try { try {
@ -62,14 +62,14 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
} }
@Override @Override
public void enableCapture() { public void hideCursor() {
super.enableCapture(); super.hideCursor();
setCursorVisibility(false); setCursorVisibility(false);
} }
@Override @Override
public void disableCapture() { public void showCursor() {
super.disableCapture(); super.showCursor();
setCursorVisibility(true); setCursorVisibility(true);
} }

View File

@ -110,6 +110,9 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
break; 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) { switch (event.type) {
case EvdevEvent.EV_SYN: case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) { if (deltaX != 0 || deltaY != 0) {
@ -231,35 +234,8 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
} }
@Override @Override
public void enableCapture() { public void showCursor() {
super.enableCapture(); super.showCursor();
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();
// This may be called on the main thread // This may be called on the main thread
runInNetworkSafeContextSynchronously(new Runnable() { runInNetworkSafeContextSynchronously(new Runnable() {
@Override @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 @Override
public void destroy() { public void destroy() {
// We need to stop the process in this context otherwise // We need to stop the process in this context otherwise