Preserve second-screen state when refreshing input profiles

This commit is contained in:
Delete228 committed 2026-11-07 00:16:45 +03:00
1 parent f44826ae3a
commit 08526f8283
3 files changed
+102 -3

No files matched your search

@@ -59,6 +59,7 @@ class SecondScreenPresentation(
private var imeSeen = false
private var fieldKeyboard = false
private var terminalPanel: SessionTerminalPanel? = null
private var controlsView: InputSurfaceView? = null
private var created = false
init {
@@ -212,6 +213,7 @@ class SecondScreenPresentation(
dropChat()
keyboardHost?.hide()
keyboardHost = null
controlsView = null
dismiss()
}
@@ -234,7 +236,14 @@ class SecondScreenPresentation(
return super.dispatchGenericMotionEvent(event)
}
fun refreshControls() { if (created) renderMode() }
fun refreshControls() {
if (!created || currentMode != SecondScreenMode.CUSTOM_CONTROLS) return
val controls = controlsView ?: return
val layout = com.droiddeck.launcher.input.InputLibraries.read(context)
.activeLayout(SessionState.mode == SessionService.MODE_DESKTOP, com.droiddeck.launcher.input.InputRole.SECONDARY)
controls.updateCustomLayout(layout?.elements.orEmpty())
controls.updateStyle(layout?.style ?: com.droiddeck.launcher.input.InputSurfaceStyle())
}
fun showKeyboard(): Boolean {
val host = keyboardHost ?: return false
@@ -263,6 +272,7 @@ class SecondScreenPresentation(
closeTerminal()
closeDashboard()
root.removeAllViews()
controlsView = null
ownComposeViews()
val library = com.droiddeck.launcher.input.InputLibraries.read(context)
val profileLayout = library.activeLayout(SessionState.mode == SessionService.MODE_DESKTOP, com.droiddeck.launcher.input.InputRole.SECONDARY)
@@ -277,8 +287,10 @@ class SecondScreenPresentation(
when (mode) {
SecondScreenMode.TERMINAL -> renderTerminal()
SecondScreenMode.CUSTOM_CONTROLS -> {
root.addView(InputSurfaceView(context, pad, profileLayout?.elements.orEmpty(),
surfaceStyle = profileLayout?.style ?: com.droiddeck.launcher.input.InputSurfaceStyle()), weightParams())
val controls = InputSurfaceView(context, pad, profileLayout?.elements.orEmpty(),
surfaceStyle = profileLayout?.style ?: com.droiddeck.launcher.input.InputSurfaceStyle())
controlsView = controls
root.addView(controls, weightParams())
attachKeyboard()
}
SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.DECK_CONTROLS -> Unit
@@ -23,6 +23,13 @@ import com.droiddeck.launcher.input.PadBridgeRoutingTest
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import org.junit.After
import com.droiddeck.launcher.input.InputLibraries
import com.droiddeck.launcher.input.InputRole
import com.droiddeck.launcher.input.InputSurfaceElement
import com.droiddeck.launcher.input.InputSurfaceStyle
import com.droiddeck.launcher.input.InputControl
import org.junit.Assert.assertSame
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -141,6 +148,74 @@ class SessionSecondScreenTest {
}
}
@Test fun profileRefreshLeavesTerminalAndDashboardIntact() {
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
val display = activity.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
val panel = SecondScreenPresentation(activity, display, {}, {}, {}, dashboard = { androidx.compose.material3.Text("Dashboard") })
try {
panel.show()
for (mode in listOf(SecondScreenMode.TERMINAL, SecondScreenMode.DASHBOARD)) {
panel.showMode(mode)
val root = panel.window!!.decorView
val before = descendants(root).toList()
val terminal: Any? = ReflectionHelpers.getField(panel, "terminalPanel")
panel.refreshControls()
assertEquals(before, descendants(root).toList())
assertSame(terminal, ReflectionHelpers.getField<Any?>(panel, "terminalPanel"))
}
} finally { panel.closeControls() }
}
@Test fun profileRefreshUpdatesControlsWithoutClosingTheFieldKeyboard() {
SessionState.mode = SessionService.MODE_STEAM
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
val display = activity.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
var closed = 0
val panel = SecondScreenPresentation(activity, display, {}, {}, {}, onKeyboardClosed = { closed++ })
try {
panel.show()
panel.showMode(SecondScreenMode.CUSTOM_CONTROLS)
panel.requestKeyboard(forField = true)
val host: Any = ReflectionHelpers.getField(panel, "keyboardHost")
val shown: MutableState<Boolean> = ReflectionHelpers.getField(panel, "keyboardShown")
shown.value = true
ReflectionHelpers.setField(panel, "imeSeen", true)
val controls = descendants(panel.window!!.decorView).filterIsInstance<InputSurfaceView>().single()
val element = InputSurfaceElement(InputControl.A, .3f, .2f, .2f, .2f, "updated")
InputLibraries.update(activity) { library ->
val layout = library.activeLayout(false, InputRole.SECONDARY)!!
library.withLayout(layout.copy(elements = listOf(element), style = InputSurfaceStyle(opacity = 35)))
}
panel.refreshControls()
assertSame(controls, descendants(panel.window!!.decorView).filterIsInstance<InputSurfaceView>().single())
val child = descendants(controls).single { it.tag == element }
assertEquals(.35f, child.alpha, .001f)
assertSame(host, ReflectionHelpers.getField<Any>(panel, "keyboardHost"))
assertTrue(panel.fieldKeyboardOpen)
assertTrue(shown.value)
assertTrue(ReflectionHelpers.getField<Boolean>(panel, "imeSeen"))
assertEquals(0, closed)
} finally { panel.closeControls() }
}
@Test fun profileRefreshPreservesTheLowerScreenChatPanel() {
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
val display = activity.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
var lost = 0
val panel = SecondScreenPresentation(activity, display, {}, {}, {})
try {
panel.show()
panel.showMode(SecondScreenMode.CUSTOM_CONTROLS)
panel.showChat(onLost = { lost++ }) { androidx.compose.material3.Text("Chat draft") }
val chat: View? = ReflectionHelpers.getField(panel, "chatView")
assertNotNull(chat)
panel.refreshControls()
assertSame(chat, ReflectionHelpers.getField<View?>(panel, "chatView"))
assertTrue(descendants(panel.window!!.decorView).any { it === chat })
assertEquals(0, lost)
} finally { panel.closeControls() }
}
@Test
@Config(sdk = [34], shadows = [PadBridgeRoutingTest.RegisteredKeyEvent::class])
fun controllerBumpersReachTheSecondScreenInBothSessionModes() {
@@ -113,6 +113,18 @@ runtime auto-update and Proton menu auto-selection. The gamescope p22 archive
was downloaded from the newly pinned component repository and verified against
upstream's SHA-256 before refreshing the local runtime inputs.
### Second-screen refresh follow-up, 2026-11-07
Refreshing a profile now updates the existing custom control surface in place.
Terminal and dashboard modes are untouched, and the custom surface keeps its
keyboard host, field-keyboard state and WoW chat panel. Actual mode changes
continue to use the existing full teardown/rebuild path.
All **35 tests in 5 suites passed** across `SessionSecondScreenTest`, the
surface UI suites and `InputSurfaceSourceTest`. Three new regressions cover
terminal/dashboard identity, changed elements/style with an open field keyboard,
and preservation of the lower-screen chat view and its dismissal callback.
## Input settings polish, 2026-11-06
The final input/UI host run passed **941 tests in 124 suites, zero failures**.