diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 289e2b10d9..8eebfc20e9 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -839,11 +839,8 @@ fun XServerScreen( !showElementEditor && !keepPausedForEditor && !showQuickMenu && !isEditMode && !container.isTouchscreenMode) { PluviaApp.touchpadView?.postDelayed({ - val view = PluviaApp.touchpadView - if (view != null) { - view.requestFocus() - view.requestPointerCapture() - } + val view = PluviaApp.touchpadView ?: return@postDelayed + view.inputCaptureManager.refreshPointerCapture() }, 100) true } else { @@ -892,13 +889,11 @@ fun XServerScreen( } PluviaApp.touchpadView?.postDelayed({ - val view = PluviaApp.touchpadView - if (view != null) { - // Delay technically not required for the function to work but this can - // race against tryCapturePointer() and end up capturing after release - // was already called - view.releasePointerCapture() - } + val view = PluviaApp.touchpadView ?: return@postDelayed + // Delay technically not required for the function to work but this can + // race against tryCapturePointer() and end up capturing after release + // was already called + view.inputCaptureManager.disablePointerCapture() }, 100) } hasUpdatedScreenGamepad = false @@ -961,6 +956,11 @@ fun XServerScreen( resumeIfAllowedAfterOverlay() } } + PluviaApp.touchpadView?.postDelayed({ + val view = PluviaApp.touchpadView ?: return@postDelayed + view.requestFocus() + view.inputCaptureManager.refreshPointerCapture() + }, 100) showQuickMenu = false } @@ -1265,13 +1265,11 @@ fun XServerScreen( controllerManager.scanForDevices() hasPhysicalController = controllerManager.getDetectedDevices().isNotEmpty() PluviaApp.touchpadView?.postDelayed({ - val view = PluviaApp.touchpadView - if (view != null) { - // Delay technically not required for the function to work but this can - // race against tryCapturePointer() and end up capturing after release - // was already called - view.releasePointerCapture() - } + val view = PluviaApp.touchpadView ?: return@postDelayed + // Delay technically not required for the function to work but this can + // race against tryCapturePointer() and end up capturing after release + // was already called + view.inputCaptureManager?.disablePointerCapture() }, 100) showQuickMenu = true @@ -1370,19 +1368,35 @@ fun XServerScreen( if (!handled) handled = xServerView!!.getxServer().winHandler.onKeyEvent(it.event) } if (!handled && isKeyboard) { - val isShiftEscPressed = it.event.keyCode == KeyEvent.KEYCODE_ESCAPE && - it.event.isShiftPressed && - it.event.action == KeyEvent.ACTION_DOWN && - it.event.repeatCount == 0 - if (isShiftEscPressed && - !showElementEditor && !keepPausedForEditor && !showQuickMenu && !isEditMode) { - gameBack() - handled = true - } else { - if (it.event.device?.isVirtual == true) { - handled = keyboard?.onVirtualKeyEvent(it.event) == true + val areAllSpecialKeysUp = it.event.isCtrlPressed and it.event.isShiftPressed and it.event.isAltPressed && it.event.action == KeyEvent.ACTION_UP + if (areAllSpecialKeysUp) { + // Handing special key combination + when (it.event.keyCode) { + KeyEvent.KEYCODE_Z -> { + // Toggles pointer exclusivity when in game view + PluviaApp.touchpadView?.inputCaptureManager?.togglePointerCapture() + handled = true + } + else -> { + handled = false + } + } + } + if (!handled) { + val isShiftEscPressed = it.event.keyCode == KeyEvent.KEYCODE_ESCAPE && + it.event.isShiftPressed && + it.event.action == KeyEvent.ACTION_DOWN && + it.event.repeatCount == 0 + if (isShiftEscPressed && + !showElementEditor && !keepPausedForEditor && !showQuickMenu && !isEditMode) { + gameBack() + handled = true } else { - handled = keyboard?.onKeyEvent(it.event) == true + if (it.event.device?.isVirtual == true) { + handled = keyboard?.onVirtualKeyEvent(it.event) == true + } else { + handled = keyboard?.onKeyEvent(it.event) == true + } } } } @@ -1418,7 +1432,6 @@ fun XServerScreen( areControlsVisible = false } } - tryCapturePointer() } } handled diff --git a/app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java b/app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java new file mode 100644 index 0000000000..4b8111f5d4 --- /dev/null +++ b/app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java @@ -0,0 +1,92 @@ +package com.winlator.inputcontrols; + +import android.view.View; + +import timber.log.Timber; + +public class InputCaptureManager { + private final View targetView; + + private boolean captureEnabled = false; + private boolean pointerCaptureRequested = true; + + public InputCaptureManager(View targetView) { + this.targetView = targetView; + } + + public boolean isCaptureEnabled() { + return captureEnabled; + } + + public void setCaptureEnabled(boolean captureEnabled) { + this.captureEnabled = captureEnabled; + } + + public boolean isPointerCaptureRequested() { + return pointerCaptureRequested; + } + + public void setPointerCaptureRequested(boolean pointerCaptureRequested) { + this.pointerCaptureRequested = pointerCaptureRequested; + } + + public void onWindowFocusChanged(@SuppressWarnings("unused") boolean hasFocus) { + refreshPointerCapture(); + } + + public void onAttachedToWindow() { + enablePointerCapture(); + } + + public void onDetachedFromWindow() { + disablePointerCapture(); + } + + public void refreshPointerCapture() { + boolean shouldCapture = targetView.hasFocus() && pointerCaptureRequested; + boolean hasCapture = targetView.hasPointerCapture(); + + if (shouldCapture && !hasCapture) { + enablePointerCapture(); + setPointerCaptureRequested(true); + } else if (!shouldCapture && hasCapture) { + disablePointerCapture(); + setPointerCaptureRequested(false); + } + } + + public void togglePointerCapture() { + if (!captureEnabled) return; + if (targetView.hasPointerCapture()) { + disablePointerCapture(); + setPointerCaptureRequested(false); + } else { + enablePointerCapture(); + setPointerCaptureRequested(true); + } + } + + public void disablePointerCapture() { + if (!captureEnabled) return; + if (!targetView.hasPointerCapture() || !pointerCaptureRequested) { + Timber.tag("TouchpadView").v("Pointer capture: Pointer capture not detected, skipped"); + return; + } + targetView.releasePointerCapture(); + Timber.tag("TouchpadView").v("Pointer capture: Pointer capture release (state=%s).", targetView.hasPointerCapture()); + } + + public void enablePointerCapture() { + if (!captureEnabled) return; + if (!targetView.hasFocus() && !targetView.requestFocus()) { + Timber.tag("TouchpadView").w("Pointer capture: Unable to request pointer capture, view is unfocused and cannot regain focus!"); + return; + } + if (targetView.hasPointerCapture() && !pointerCaptureRequested) { + Timber.v("Pointer capture: Pointer capture already requested, skipped"); + return; + } + targetView.requestPointerCapture(); + Timber.tag("TouchpadView").v("Pointer capture: Pointer capture request (state=%s).", targetView.hasPointerCapture()); + } +} diff --git a/app/src/main/java/com/winlator/widget/TouchpadView.java b/app/src/main/java/com/winlator/widget/TouchpadView.java index f387ff7166..1d703e685f 100644 --- a/app/src/main/java/com/winlator/widget/TouchpadView.java +++ b/app/src/main/java/com/winlator/widget/TouchpadView.java @@ -17,6 +17,7 @@ import app.gamenative.R; import app.gamenative.data.TouchGestureConfig; import com.winlator.core.AppUtils; +import com.winlator.inputcontrols.InputCaptureManager; import com.winlator.math.Mathf; import com.winlator.math.XForm; import com.winlator.renderer.ViewTransformation; @@ -62,8 +63,8 @@ public class TouchpadView extends View implements View.OnCapturedPointerListener private Runnable delayedPress; private boolean pressExecuted; - private final boolean capturePointerOnExternalMouse; - private boolean pointerCaptureRequested; + + private final InputCaptureManager inputCaptureManager; // Suppress spurious left-click after two-finger right-click tap private boolean suppressNextLeftTap; @@ -197,23 +198,12 @@ private void stopGestureRefresh() { } } - @Override - protected void onDetachedFromWindow() { - // Full cleanup on detach: cancels timers, refresh runnable, releases - // any held drag/long-press/2F/3F-hold injections, and resets gesture - // state. Avoids leaking pressed buttons/keys when the view is removed - // mid-gesture (e.g., game exit while a hold is active). - handleTsCancel(); - super.onDetachedFromWindow(); - } - // Left/right click drag tracking private boolean leftClickDragButtonDown; private boolean rightClickDragButtonDown; public TouchpadView(Context context, XServer xServer, boolean capturePointerOnExternalMouse) { super(context); - this.capturePointerOnExternalMouse = capturePointerOnExternalMouse; this.fingers = new Finger[4]; this.numFingers = (byte) 0; this.sensitivity = 1.0f; @@ -237,17 +227,39 @@ public TouchpadView(Context context, XServer xServer, boolean capturePointerOnEx int screenHeight = AppUtils.getScreenHeight(); ScreenInfo screenInfo = xServer.screenInfo; updateXform(screenWidth, screenHeight, screenInfo.width, screenInfo.height); + this.inputCaptureManager = new InputCaptureManager(this); if (capturePointerOnExternalMouse) { + inputCaptureManager.setCaptureEnabled(true); setFocusableInTouchMode(true); setOnCapturedPointerListener(this); } } + public InputCaptureManager getInputCaptureManager() { + return inputCaptureManager; + } + @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - // allow re-capture after app returns from background - if (hasFocus) pointerCaptureRequested = false; + this.inputCaptureManager.onWindowFocusChanged(hasFocus); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.inputCaptureManager.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + // Full cleanup on detach: cancels timers, refresh runnable, releases + // any held drag/long-press/2F/3F-hold injections, and resets gesture + // state. Avoids leaking pressed buttons/keys when the view is removed + // mid-gesture (e.g., game exit while a hold is active). + handleTsCancel(); + super.onDetachedFromWindow(); + this.inputCaptureManager.onDetachedFromWindow(); } private static StateListDrawable createTransparentBackground() {