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
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-11-09 15:02:12 -05:00
1 parent 7c89c14a84
commit 75a0230304
5 files changed
+59 -2

No files matched your search

@@ -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()
@@ -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) }
@@ -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
}
@@ -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}"""
@@ -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)))
}
}