From 75a0230304d8f40478ef7b05095e109d504bcf4d Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Mon, 9 Nov 2026 15:02:12 -0500 Subject: [PATCH] fix(input): Auto relative mouse stays off for direct touch without a mouse (#2305) * fix(input): Auto relative mouse stays off for direct touch without a mouse * fix(input): re-check relative mouse when an input device changes * fix(input): reapply relative mouse after agent readiness * fix(input): only the current session clears the agent-ready hook and readiness resets relative mouse retries --- .../com/droiddeck/launcher/SessionActivity.kt | 20 ++++++++++++++-- .../droiddeck/launcher/agent/AgentGuest.kt | 2 ++ .../launcher/input/TouchGestureConfig.kt | 3 +++ .../launcher/input/GesturePrefsTest.kt | 13 +++++++++++ .../launcher/input/TouchRouterTest.kt | 23 +++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/com/droiddeck/launcher/input/TouchRouterTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index e102b1e12..8deb14120 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -286,6 +286,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var directHoldBehavior by mutableStateOf(com.droiddeck.launcher.input.TouchGestureConfig.MOUSE_BEHAVIOR_CLICK) private var relativeMouseMode by mutableIntStateOf(0) @Volatile private var appliedRelativeMouseMode: Int? = null + private val agentReadyListener: () -> Unit = { relativeMouseRetries = 0; requestRelativeMouse() } private val appTextFocus get() = wowChat.request != null private val wowChat by lazy { com.droiddeck.launcher.ui.WowChat(this, ::sendWowChat, ::cancelWowChat, ::readGameField) } private var wowChatBottom: SecondScreenPresentation? = null @@ -636,6 +637,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { padBridge?.syncConnectedControllers() refreshControls() updatePointerCapture() + requestRelativeMouse() updateHandCamera() refreshSecondScreenDisplays() } @@ -651,12 +653,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (::padMouse.isInitialized) padMouse.reset() refreshControls() updatePointerCapture() + requestRelativeMouse() updateHandCamera() refreshSecondScreenDisplays() } override fun onInputDeviceChanged(deviceId: Int) { joyConVr?.refresh() refreshControls() + requestRelativeMouse() refreshSecondScreenDisplays() } } @@ -665,6 +669,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + AgentGuest.readyListener = agentReadyListener if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isRemoving() || com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isMaintaining()) { runtimeRemovalBlocked = true @@ -1960,6 +1965,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val nextRoutedTouchMode = effectiveTouchMode() if (routedTouchMode != nextRoutedTouchMode) gestureTouchStreamGate.reset() routedTouchMode = nextRoutedTouchMode + requestRelativeMouse() frameGen = FrameGen.mode(this) fexPreset = SessionPrefs.fexPreset(this) suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode) @@ -2981,7 +2987,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun applyRelativeMouse() { - val mode = desiredRelativeMouseMode + val wanted = desiredRelativeMouseMode + val touchpadRoute = com.droiddeck.launcher.input.TouchRouter.decide(com.droiddeck.launcher.input.TouchRouter.Snapshot( + uiBlocking = false, + mode = routedTouchMode, + scrollLayer = false, + steamSession = SessionState.mode == SessionService.MODE_STEAM, + steamDesktopUi = SessionState.steamUi == "desktop", + )) == com.droiddeck.launcher.input.TouchRouter.Route.TOUCHPAD + val mode = com.droiddeck.launcher.input.TouchGestureConfig.gamescopeRelativeMouse(wanted, mouseAttached(), touchpadRoute) if (appliedRelativeMouseMode == mode) return val request = org.json.JSONObject() .put("kind", "exec") @@ -2997,7 +3011,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { Log.w(TAG, "gamescope did not take relative mouse $mode: ${result.exceptionOrNull() ?: answer}") if (relativeMouseRetries++ < RELATIVE_MOUSE_MAX_RETRIES) uiHandler.postDelayed({ requestRelativeMouse() }, 2_000) } - if (desiredRelativeMouseMode != mode) requestRelativeMouse() + if (desiredRelativeMouseMode != wanted) requestRelativeMouse() } private fun refreshGestureConfig() { @@ -3813,6 +3827,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (next == SessionPrefs.TOUCH_OFF) cursorVisible = false gestureTouchStreamGate.reset() routedTouchMode = next + requestRelativeMouse() if (directTouchCursorHidden()) cursorVisible = false } @@ -5319,6 +5334,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } override fun onDestroy() { + if (AgentGuest.readyListener === agentReadyListener) AgentGuest.readyListener = null com.droiddeck.launcher.input.HomeButtonShortcuts.detach(this) relativeMouseRequests.shutdownNow() backQamInput.cancel() diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt index eaa2bbc0f..e04dda258 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt @@ -33,6 +33,7 @@ object AgentGuest { var keyboardDismissals by mutableIntStateOf(0) private set @Volatile private var keyboardRoute = false + @Volatile var readyListener: (() -> Unit)? = null @Volatile private var routeDir: File? = null private const val TAG = "AgentGuest" private const val POLL_MS = 50L @@ -59,6 +60,7 @@ object AgentGuest { when (path) { "hello.json" -> readJson(File(dir, path))?.let { SessionEvents.record("agent.ready", mapOf("pid" to it.optInt("pid"), "focus" to it.optBoolean("focus"))) + readyListener?.invoke() } "focus.json" -> readJson(File(dir, path))?.let(::recordFocus) "keyboard.json" -> readJson(File(dir, path))?.let { recordKeyboard(this, it) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/TouchGestureConfig.kt b/app/src/main/java/com/droiddeck/launcher/input/TouchGestureConfig.kt index d3be47056..035060585 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/TouchGestureConfig.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/TouchGestureConfig.kt @@ -213,6 +213,9 @@ data class TouchGestureConfig( return if (value == MOUSE_BEHAVIOR_CLICK) MOUSE_BEHAVIOR_CLICK else MOUSE_BEHAVIOR_HOLD } + fun gamescopeRelativeMouse(mode: Int, mouseAttached: Boolean, touchpad: Boolean): Int = + if (mode == RELATIVE_MOUSE_AUTO && !mouseAttached && !touchpad) RELATIVE_MOUSE_OFF else mode + private fun normalizeRelativeMouseMode(mode: Int): Int { return if (mode in RELATIVE_MOUSE_AUTO..RELATIVE_MOUSE_OFF) mode else RELATIVE_MOUSE_AUTO } diff --git a/app/src/test/java/com/droiddeck/launcher/input/GesturePrefsTest.kt b/app/src/test/java/com/droiddeck/launcher/input/GesturePrefsTest.kt index 9f0381112..a00594f78 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/GesturePrefsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/GesturePrefsTest.kt @@ -17,6 +17,19 @@ class GesturePrefsTest { assertEquals(TouchGestureConfig.RELATIVE_MOUSE_AUTO, TouchGestureConfig.fromJson("{}").relativeMouseMode) } + @Test fun autoNeverGoesRelativeForDirectTouchWithoutAMouse() { + val auto = TouchGestureConfig.RELATIVE_MOUSE_AUTO + assertEquals(TouchGestureConfig.RELATIVE_MOUSE_OFF, TouchGestureConfig.gamescopeRelativeMouse(auto, mouseAttached = false, touchpad = false)) + assertEquals(auto, TouchGestureConfig.gamescopeRelativeMouse(auto, mouseAttached = true, touchpad = false)) + assertEquals(auto, TouchGestureConfig.gamescopeRelativeMouse(auto, mouseAttached = false, touchpad = true)) + } + + @Test fun aChosenRelativeMouseModeIsSentAsIs() { + for (mode in listOf(TouchGestureConfig.RELATIVE_MOUSE_ON, TouchGestureConfig.RELATIVE_MOUSE_OFF)) + for (mouse in listOf(false, true)) for (pad in listOf(false, true)) + assertEquals(mode, TouchGestureConfig.gamescopeRelativeMouse(mode, mouse, pad)) + } + @Test fun removedOptionsAreIgnoredWhenLoadingExistingSettings() { val old = """{"holdMouseButtonWhileTouchingEnabled":true,"holdMouseButtonWhileTouchingAction":"right_click", "showClickHighlight":true,"showCursorInTouchscreenMode":true,"tapAction":"key_E","longPressDelay":650}""" diff --git a/app/src/test/java/com/droiddeck/launcher/input/TouchRouterTest.kt b/app/src/test/java/com/droiddeck/launcher/input/TouchRouterTest.kt new file mode 100644 index 000000000..38b270e09 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/input/TouchRouterTest.kt @@ -0,0 +1,23 @@ +package com.droiddeck.launcher.input + +import com.droiddeck.launcher.session.SessionPrefs +import org.junit.Assert.assertEquals +import org.junit.Test + +class TouchRouterTest { + @Test fun gesturesAndOffAreNotTouchpadRoutes() { + for (mode in listOf(SessionPrefs.TOUCH_GESTURES, SessionPrefs.TOUCH_OFF)) { + assertEquals(TouchRouter.Route.GESTURES.takeIf { mode == SessionPrefs.TOUCH_GESTURES } ?: TouchRouter.Route.OFF, + TouchRouter.decide(TouchRouter.Snapshot(false, mode, false, false, false))) + assertEquals(TouchRouter.Route.GESTURES.takeIf { mode == SessionPrefs.TOUCH_GESTURES } ?: TouchRouter.Route.OFF, + TouchRouter.decide(TouchRouter.Snapshot(false, mode, false, true, true))) + } + } + + @Test fun autoUsesTouchpadOnlyWhenRouterSelectsTouchpad() { + assertEquals(TouchRouter.Route.TOUCHPAD, + TouchRouter.decide(TouchRouter.Snapshot(false, SessionPrefs.TOUCH_AUTO, false, false, false))) + assertEquals(TouchRouter.Route.DIRECT, + TouchRouter.decide(TouchRouter.Snapshot(false, SessionPrefs.TOUCH_AUTO, false, true, false))) + } +}