mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-08 01:48:19 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2982e474ab |
No files matched your search
@@ -785,11 +785,18 @@ jobs:
|
||||
cd app/src/main/rust/stores
|
||||
cargo test --locked 2>&1 | tail -60
|
||||
|
||||
python-tests:
|
||||
name: Python and native regression tests
|
||||
python-runner:
|
||||
needs: source
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
uses: ./.github/workflows/pick-runner.yml
|
||||
secrets:
|
||||
RUNNER_STATUS_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
|
||||
|
||||
python-tests:
|
||||
name: Python and native regression tests
|
||||
needs: [source, python-runner]
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ${{ fromJSON(needs.python-runner.outputs.labels) }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -603,19 +603,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}
|
||||
|
||||
fun selectDriver(id: String, linux: Boolean) {
|
||||
if (!linux && id == TurnipDriver.AUTO) {
|
||||
DriverBundle.selectDisplay(activity, id)
|
||||
pairModeOpen = false
|
||||
lastAutoCheck = 0L
|
||||
setMode(auto = true)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
val bundle = DriverBundle.containing(activity, id, linux)
|
||||
when {
|
||||
bundle != null -> useBundle(bundle)
|
||||
linux -> SessionPrefs.setLinuxDriver(activity, id)
|
||||
else -> DriverBundle.selectDisplay(activity, id)
|
||||
else -> SessionPrefs.setAndroidDriver(activity, id)
|
||||
}
|
||||
pairModeOpen = false
|
||||
setMode(auto = false, mix = true)
|
||||
|
||||
@@ -23,7 +23,6 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -874,7 +873,6 @@ class FrontEndActivity : ComponentActivity() {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
Library.recordPlayed(this, game)
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.putExtra(SessionActivity.EXTRA_STEAMVR, steamVr)
|
||||
.returnTo(returnHome)
|
||||
@@ -890,15 +888,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
putExtra(SessionService.EXTRA_STEAM_URL,
|
||||
com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(game.gameIdString))
|
||||
}
|
||||
}
|
||||
.playing(game), steamSession = true)
|
||||
}
|
||||
|
||||
private fun Intent.playing(game: Library.SteamGame): Intent =
|
||||
apply { if (game.library == Library.ADDED) putExtra(SessionActivity.EXTRA_PLAYED_GAME, game.gameIdString) }
|
||||
|
||||
private fun recordPlayed(intent: Intent) {
|
||||
intent.getStringExtra(SessionActivity.EXTRA_PLAYED_GAME)?.let { Library.recordPlayed(this, it) }
|
||||
}, steamSession = true)
|
||||
}
|
||||
|
||||
private fun Intent.returnTo(returnHome: Boolean): Intent {
|
||||
@@ -925,17 +915,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
.putExtra(SessionService.EXTRA_PROGRAM, command.program)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM_ARGS, command.args.toTypedArray())
|
||||
.putExtra(SessionService.EXTRA_GAME_ID, com.droiddeck.launcher.session.NoSteamLaunch.scope(game))
|
||||
.playing(game)
|
||||
when {
|
||||
!SessionState.running -> startSessionOrWait(next)
|
||||
SessionState.stopRequested -> android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
command.program == com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET && handToBattleNet(command.args, next, returnHome) -> {}
|
||||
SessionState.mode == SessionService.MODE_RUN && SessionState.program == command.program && SessionState.programArgs == command.args -> {
|
||||
recordPlayed(next)
|
||||
command.program == com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET && handToBattleNet(command.args, returnHome) { pendingSwitch = next } -> {}
|
||||
SessionState.mode == SessionService.MODE_RUN && SessionState.program == command.program && SessionState.programArgs == command.args ->
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
else -> pendingSwitch = next
|
||||
}
|
||||
}
|
||||
@@ -1136,10 +1123,6 @@ class FrontEndActivity : ComponentActivity() {
|
||||
applyLauncherFullscreen()
|
||||
updates.start()
|
||||
setContent {
|
||||
val prootPack by com.droiddeck.launcher.core.AddonPack.PROOT.state.collectAsState()
|
||||
androidx.compose.runtime.LaunchedEffect(prootPack) {
|
||||
if (prootPack == com.droiddeck.launcher.core.AddonPack.State.Ready && installed == null) refresh()
|
||||
}
|
||||
DroidDeckTheme(theme, appScale = appScale) {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize().exposeTestTags()) {
|
||||
if (decky.deckyPendingZip != null) com.droiddeck.launcher.ui.DeckyZipWarningDialog(
|
||||
@@ -2293,14 +2276,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
when {
|
||||
!SessionState.running -> startSessionOrWait(next)
|
||||
SessionState.stopRequested -> android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
handToBattleNet(full, next) -> {}
|
||||
handToBattleNet(full) { pendingSwitch = next } -> {}
|
||||
else -> pendingSwitch = next
|
||||
}
|
||||
}
|
||||
}, "battlenet-open").start()
|
||||
}
|
||||
|
||||
private fun handToBattleNet(args: List<String>, next: Intent, returnHome: Boolean = false): Boolean {
|
||||
private fun handToBattleNet(args: List<String>, returnHome: Boolean = false, otherwise: () -> Unit): Boolean {
|
||||
if (SessionState.mode != SessionService.MODE_RUN || SessionState.program != com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET) return false
|
||||
val library = args.takeIf { it.firstOrNull() == "--library" }?.getOrNull(1)
|
||||
val sessionLibrary = SessionState.programArgs.takeIf { it.firstOrNull() == "--library" }?.getOrNull(1)
|
||||
@@ -2311,13 +2294,10 @@ class FrontEndActivity : ComponentActivity() {
|
||||
val handed = request.isEmpty() || com.droiddeck.launcher.stores.battlenet.BattleNetClient.request(LinuxRuntime.sessionRoot(app), request)
|
||||
ui.post {
|
||||
if (isDestroyed) return@post
|
||||
if (!handed) pendingSwitch = next
|
||||
else {
|
||||
recordPlayed(next)
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
if (!handed) otherwise()
|
||||
else startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
}, "battlenet-request").start()
|
||||
return true
|
||||
|
||||
@@ -178,9 +178,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
private var unfoldedPanel by mutableStateOf<OnScreenControls?>(null)
|
||||
private var unfoldedColumn: android.widget.LinearLayout? = null
|
||||
private var unfoldedDeckSurface: com.droiddeck.launcher.ui.InputSurfaceView? = null
|
||||
private var unfoldedDeckPanel by mutableStateOf<com.droiddeck.launcher.input.DeckPanel?>(null)
|
||||
private var deckPanelLayouts: Map<com.droiddeck.launcher.input.DeckPanel, com.droiddeck.launcher.input.SavedInputLayout?> = emptyMap()
|
||||
private lateinit var sessionRoot: FrameLayout
|
||||
private var inputRouter: InputRouter? = null
|
||||
private var physicalInputBindings: com.droiddeck.launcher.input.PhysicalInputBindings? = null
|
||||
@@ -568,7 +565,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var sessionActivityDisplayId = Display.DEFAULT_DISPLAY
|
||||
private lateinit var displayManager: DisplayManager
|
||||
private var secondScreenPresentation: SecondScreenPresentation? = null
|
||||
private val inputWindowDisplays = mutableListOf<Int>()
|
||||
private val bottomScreenListener: () -> Unit = {
|
||||
if (secondScreenPresentation?.display?.displayId?.let(com.droiddeck.launcher.input.DualScreen::taken) == true) closeSecondScreen(reset = false)
|
||||
updateBottomGame()
|
||||
@@ -981,7 +977,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
} else {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize()) {
|
||||
CursorOverlay({ cursorPos }, cursorVisible && (!uiPointer || onlyPcKeyboardOpen()) && !bottomOutputActive,
|
||||
CursorOverlay({ cursorPos }, cursorVisible && !uiPointer && !bottomOutputActive,
|
||||
resources.displayMetrics.density,
|
||||
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
|
||||
if (memoryHud.text.isNotEmpty()) HudText(memoryHud.text, hudColor, hudOpacity)
|
||||
@@ -1226,7 +1222,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
textureLodBias = if (drawerGameProfileId == null) textureLodBias else drawerTextureLodBias,
|
||||
frameGen = frameGen,
|
||||
lossless = lossless,
|
||||
oscMode = oscMode, suspendPolicy = suspendPolicy,
|
||||
oscMode = oscMode, onScreenButtonsVisible = onScreenButtonsVisible, suspendPolicy = suspendPolicy,
|
||||
steamDownloadsInBackground = steamDownloadsInBackground, touchMode = touchMode,
|
||||
micAvailable = SessionState.micAvailable && !SessionState.suspended,
|
||||
micEnabled = SessionState.micEnabled,
|
||||
@@ -1447,7 +1443,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
},
|
||||
onClose = { drawerOpen = false },
|
||||
onDismissScreen = if (docked) ({ selectSecondScreenMode(SecondScreenMode.NONE) }) else null,
|
||||
components = drawerComponents,
|
||||
onComponentsRefresh = {
|
||||
refreshDrawerComponents()
|
||||
@@ -2230,7 +2225,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
intent.getStringExtra(SessionService.EXTRA_GAME_ID),
|
||||
sessionActivityDisplayId,
|
||||
)
|
||||
intent.getStringExtra(EXTRA_PLAYED_GAME)?.let { com.droiddeck.launcher.frontend.Library.recordPlayed(this, it) }
|
||||
}
|
||||
applyFrameGen()
|
||||
applyGameAlignment()
|
||||
@@ -2321,11 +2315,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::directTouchHold.isInitialized) directTouchHold.cancel()
|
||||
if (::twoFingerGestures.isInitialized) twoFingerGestures.cancel()
|
||||
if (::scrollTouch.isInitialized) scrollTouch.cancel()
|
||||
scrollTouchGesture = false
|
||||
gesturePointerReady = false
|
||||
if (sessionTouchDown) dismissTouch = true
|
||||
}
|
||||
|
||||
private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) {
|
||||
@@ -2834,7 +2826,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return onScreenControls?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
steamTouchControls?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedPanel?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedDeckSurface?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedColumn?.getChildAt(0)?.let { takes(it) { _, _ -> true } } == true
|
||||
}
|
||||
|
||||
@@ -3387,10 +3378,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
var presentation = secondScreenPresentation
|
||||
if (presentation?.display?.displayId != target.displayId) {
|
||||
if (target.displayId in inputWindowDisplays) {
|
||||
panelMode(mode)
|
||||
return
|
||||
}
|
||||
dropDockedDrawer()
|
||||
presentation?.closeControls()
|
||||
presentation = secondScreenPresentation(target)
|
||||
@@ -3531,15 +3518,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val main = window.decorView.display
|
||||
if (role == InputRole.PRIMARY) return bottomGame?.display ?: main
|
||||
if (bottomGame != null) return main
|
||||
return com.droiddeck.launcher.input.SecondScreenDisplays.editorCandidates(selectedSecondScreenDisplay,
|
||||
secondScreenDisplays.map { it.id }, com.droiddeck.launcher.input.DualScreen::taken).firstNotNullOfOrNull(displayManager::getDisplay)
|
||||
}
|
||||
|
||||
fun inputWindowShown(displayId: Int) { inputWindowDisplays += displayId }
|
||||
|
||||
fun inputWindowClosed(displayId: Int) {
|
||||
inputWindowDisplays -= displayId
|
||||
if (started && displayId !in inputWindowDisplays && secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
|
||||
return displayManager.getDisplay(selectedSecondScreenDisplay)
|
||||
?: secondScreenDisplays.firstOrNull()?.let { displayManager.getDisplay(it.id) }
|
||||
}
|
||||
|
||||
private fun refreshInputProfile() {
|
||||
@@ -3550,7 +3530,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
adaptiveSticks = primaryInputLayout?.adaptiveSticks
|
||||
quickButtons = primaryInputLayout?.takeIf { it.editable }?.let { l -> l.quickButtons.associateWith { it !in l.style.hidden } }.orEmpty()
|
||||
secondaryInputLayout = library.panelLayout(desktop, secondScreenMode.panel(this))
|
||||
deckPanelLayouts = com.droiddeck.launcher.input.DeckPanel.entries.associateWith { library.panelLayout(desktop, it.bindings) }
|
||||
inputUsesLocalActions = InputControl.padControls.any { inputProfile.action(InputRole.PHYSICAL, it) !is InputAction.Native }
|
||||
inputUsesPointer = inputProfile.bindings.values.any { bindings ->
|
||||
bindings.values.any { it is InputAction.Pointer || it is InputAction.MouseButton || it is InputAction.Scroll }
|
||||
@@ -3564,8 +3543,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onScreenControls?.updateLayout(primaryInputLayout)
|
||||
bottomInputControls?.updateLayout(primaryInputLayout)
|
||||
unfoldedPanel?.updateLayout(primaryInputLayout)
|
||||
if (unfoldedPanel != null) applyUnfoldedControls()
|
||||
showUnfoldedDeckPanel()
|
||||
updateOnScreenPad()
|
||||
secondScreenPresentation?.refreshControls()
|
||||
}
|
||||
@@ -3617,15 +3594,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
Log.i(TAG, if (wanted) "unfolded controls: game ${width}x$gameHeight, controls ${width}x${height - gameHeight}"
|
||||
else "unfolded controls: game full screen")
|
||||
}
|
||||
val portrait = !com.droiddeck.launcher.session.SessionDisplay.foldable(this)
|
||||
val dock = if (SessionState.deckPad) com.droiddeck.launcher.input.OnScreenLayout.Dock.deck(portrait, deckPanelLayouts[com.droiddeck.launcher.input.DeckPanel.GAMEPAD])
|
||||
else com.droiddeck.launcher.input.OnScreenLayout.Dock(portrait, false)
|
||||
val dock = com.droiddeck.launcher.input.OnScreenLayout.Dock(!com.droiddeck.launcher.session.SessionDisplay.foldable(this), SessionState.deckPad)
|
||||
var panel = unfoldedPanel
|
||||
if (panel != null && (!wanted || panel.dock != dock)) {
|
||||
sessionRoot.removeView(unfoldedColumn)
|
||||
unfoldedColumn = null
|
||||
unfoldedDeckSurface = null
|
||||
unfoldedDeckPanel = null
|
||||
unfoldedPanel = null
|
||||
panel = null
|
||||
if (!wanted) updateOnScreenControls()
|
||||
@@ -3642,36 +3615,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
sessionRoot.addView(column, sessionRoot.indexOfChild(surfaceView) + 1)
|
||||
unfoldedColumn = column
|
||||
unfoldedPanel = panel
|
||||
showUnfoldedDeckPanel()
|
||||
updateOnScreenControls()
|
||||
}
|
||||
unfoldedColumn?.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, height - gameHeight, Gravity.BOTTOM)
|
||||
}
|
||||
|
||||
private fun unfoldedBar(): View = com.droiddeck.launcher.ui.bottomScreenBarView(this) {
|
||||
val deck = unfoldedDeckPanel
|
||||
com.droiddeck.launcher.ui.sessionBarModel(getString(if (deck != null) R.string.second_mode_deck_controls else R.string.deck_tab_gamepad),
|
||||
{ chooseUnfoldedControls(false) }, { drawerOpen = true }, ::tapSteamButton, ::tapQam,
|
||||
tabs = deck?.let { com.droiddeck.launcher.ui.deckPanelTabs(this, it, ::selectDeckPanel) })
|
||||
}
|
||||
|
||||
private fun showUnfoldedDeckPanel() {
|
||||
val column = unfoldedColumn
|
||||
val panel = unfoldedPanel
|
||||
val selected = SessionPrefs.deckPanel(this).takeIf { column != null && panel?.dock?.grips == true }
|
||||
unfoldedDeckPanel = selected
|
||||
val layout = selected?.takeIf { it != com.droiddeck.launcher.input.DeckPanel.GAMEPAD }?.let { deckPanelLayouts[it] }
|
||||
if (layout != null && unfoldedDeckSurface?.tag == layout) return
|
||||
unfoldedDeckSurface?.let { it.releaseInputs(); column?.removeView(it) }
|
||||
unfoldedDeckSurface = null
|
||||
if (column == null || panel == null) return
|
||||
if (layout == null) { panel.visibility = View.VISIBLE; return }
|
||||
panel.releaseAll()
|
||||
panel.visibility = View.GONE
|
||||
unfoldedDeckSurface = com.droiddeck.launcher.ui.InputSurfaceView(this, padBridge, layout.elements, InputRole.PRIMARY, layout.style, haptics = true).also {
|
||||
it.tag = layout
|
||||
column.addView(it, android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f))
|
||||
}
|
||||
com.droiddeck.launcher.ui.sessionBarModel(getString(R.string.deck_tab_gamepad), { chooseUnfoldedControls(false) },
|
||||
{ drawerOpen = true }, ::tapSteamButton, ::tapQam)
|
||||
}
|
||||
|
||||
private fun bottomKeyboardPresentation(): SecondScreenPresentation? {
|
||||
@@ -3967,17 +3918,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var lockedSwipeX = 0f
|
||||
private var lockedSwipeY = 0f
|
||||
private var dismissTouch = false
|
||||
private var sessionTouchDown = false
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event)
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
sessionTouchDown = true
|
||||
dismissTouch = false
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> sessionTouchDown = false
|
||||
}
|
||||
if (drawerOpen && drawerDocked && event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
drawerLayerOpen = false
|
||||
drawerOpenState = false
|
||||
@@ -3989,7 +3932,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) dismissTouch = false
|
||||
return true
|
||||
}
|
||||
val uiBlocking = drawerOpen || achievementsOpen || gestureSettingsOpen || inputSettingsOpen || pipUi || SessionState.suspended
|
||||
val uiBlocking = drawerOpen || pcKeyboardOpen || achievementsOpen || gestureSettingsOpen || inputSettingsOpen || pipUi || SessionState.suspended
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN && !uiBlocking && routedTouchMode != SessionPrefs.TOUCH_GESTURES) {
|
||||
scrollTouchGesture = scrollTouchMode()
|
||||
scrollRestX = 0f
|
||||
@@ -4017,7 +3960,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
com.droiddeck.launcher.input.TouchRouter.Route.SCROLL -> scrollTouch.onTouch(event)
|
||||
else -> {
|
||||
if (com.droiddeck.launcher.input.TouchRouter.steamKeyboardZone(snapshot)) {
|
||||
val wasActive = threeFingerTap.active && event.actionMasked != MotionEvent.ACTION_DOWN
|
||||
val wasActive = threeFingerTap.active
|
||||
if (threeFingerTap.onTouch(event)) padBridge?.triggerSteamKeyboard()
|
||||
if (threeFingerTap.active && !wasActive) {
|
||||
if (route == com.droiddeck.launcher.input.TouchRouter.Route.TOUCHPAD) touchpad.cancel()
|
||||
@@ -4093,10 +4036,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return true
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) lockedSwipeUsed = false
|
||||
if (lockedSwipeUsed && (event.actionMasked != MotionEvent.ACTION_MOVE || !programLocksPointer())) return true
|
||||
if (programLocksPointer() && event.actionMasked == MotionEvent.ACTION_MOVE) {
|
||||
if (lockedSwipeUsed) directTouchHold.cancelPendingHold() else directTouchHold.cancelTouch()
|
||||
lockedSwipeUsed = true
|
||||
directTouchHold.cancelPendingHold()
|
||||
val scale = sceneScale(rect)
|
||||
sendSceneDelta((event.x - swipeX) * scale, (event.y - swipeY) * scale)
|
||||
return true
|
||||
@@ -4213,9 +4155,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
applyPlatformPointer(appUiOwnsPointer())
|
||||
}
|
||||
|
||||
private fun onlyPcKeyboardOpen(): Boolean =
|
||||
pcKeyboardOpen && !drawerOpen && !achievementsOpen && !inputSettingsOpen && !pipUi && !glassesOutputActive && !touchEditorOpen && !wowChatOnTop
|
||||
|
||||
private fun appUiOwnsPointer(): Boolean =
|
||||
drawerOpen || pcKeyboardOpen || achievementsOpen || inputSettingsOpen || pipUi || glassesOutputActive || bottomOutputActive || touchEditorOpen || wowChatOnTop
|
||||
|
||||
@@ -4306,7 +4245,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
updateOnScreenPad()
|
||||
val c = onScreenControls
|
||||
glassesSlot = when {
|
||||
c == null || c.visibility != View.VISIBLE || c.isPinnedOnly -> 0
|
||||
c == null || c.visibility != View.VISIBLE -> 0
|
||||
c.isButtonsOnly -> -1
|
||||
primaryInputLayout?.let { l -> l.elements.any { it.quickButton == InputCommand.KEYBOARD && !l.style.hides(it) } }
|
||||
?: (controllerSettings?.keyboardButton != false) -> 2
|
||||
@@ -4321,17 +4260,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val controls = onScreenControls ?: return
|
||||
fun showPinned() {
|
||||
controls.setPinnedOnly(true)
|
||||
val pinned = controls.hasPinned && !pipUi && !inputSettingsOpen
|
||||
if (!pinned) controls.releaseAll()
|
||||
controls.visibility = if (pinned) View.VISIBLE else View.GONE
|
||||
}
|
||||
if (pipUi || unfoldedPanel != null || pcKeyboardOpen || inputSettingsOpen ||
|
||||
(SessionState.mode == SessionService.MODE_STEAM && !SessionPrefs.builtinPad(this))) {
|
||||
onScreenButtonsVisible = false
|
||||
hideBottomControls()
|
||||
showPinned()
|
||||
controls.releaseAll()
|
||||
controls.visibility = View.GONE
|
||||
steamTouchControls?.let { it.cancelEditing(); it.releaseAll(); it.visibility = View.GONE }
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
|
||||
applyTouchRouting()
|
||||
@@ -4364,7 +4298,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val drawn = show && !keyboardCoversPad()
|
||||
if (steam != null) {
|
||||
hideBottomControls()
|
||||
showPinned()
|
||||
if (controls.visibility == View.VISIBLE) { controls.releaseAll(); controls.visibility = View.GONE }
|
||||
if (!drawn) { steam.cancelEditing(); steam.releaseAll() }
|
||||
steam.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = show
|
||||
@@ -4373,7 +4307,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return
|
||||
}
|
||||
val bottomRoot = bottomGame?.surfaceView?.parent as? android.view.ViewGroup
|
||||
if (bottomRoot != null && (drawn || controls.hasPinned)) {
|
||||
if (drawn && bottomRoot != null) {
|
||||
controls.releaseAll(); controls.visibility = View.GONE
|
||||
var bottom = bottomInputControls
|
||||
if (bottom?.parent !== bottomRoot) {
|
||||
@@ -4386,14 +4320,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
bottom.updateLayout(primaryInputLayout)
|
||||
bottom.setButtonsOnly(buttonsOnly)
|
||||
bottom.setPinnedOnly(!drawn)
|
||||
bottom.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
hideBottomControls()
|
||||
val wasDrawn = controls.visibility == View.VISIBLE && !controls.isPinnedOnly
|
||||
if (!drawn) showPinned() else { controls.setPinnedOnly(false); controls.visibility = View.VISIBLE }
|
||||
if (drawn == wasDrawn) return
|
||||
if (drawn == (controls.visibility == View.VISIBLE)) return
|
||||
if (!drawn) controls.releaseAll()
|
||||
controls.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
Log.i(TAG, "on-screen controls " + when {
|
||||
!drawn -> "hidden"
|
||||
buttonsOnly -> "Steam + QAM"
|
||||
@@ -5136,7 +5069,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(SecondScreenMode.KEYBOARD_TRACKPAD.label), listOf(keyboardExtra)))
|
||||
SecondScreenMode.DECK_CONTROLS -> com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(SecondScreenMode.DECK_CONTROLS.label),
|
||||
tabs = com.droiddeck.launcher.ui.deckPanelTabs(this@SessionActivity, SessionPrefs.deckPanel(this@SessionActivity), ::selectDeckPanel)))
|
||||
else -> com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(secondScreenMode.label)))
|
||||
else -> Unit
|
||||
}
|
||||
androidx.compose.runtime.key(profileLayout) {
|
||||
androidx.compose.ui.viewinterop.AndroidView(
|
||||
@@ -5457,7 +5390,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
const val EXTRA_RETURN_HOME = "returnHome"
|
||||
const val EXTRA_LINK_TASK = "linkTask"
|
||||
const val EXTRA_STEAMVR = "steamVr"
|
||||
const val EXTRA_PLAYED_GAME = "com.droiddeck.launcher.PLAYED_GAME"
|
||||
const val EXTRA_REPAIR_RUNTIME = "repairRuntime"
|
||||
private const val LEAVE_TIMEOUT_MS = 1_500L
|
||||
private const val TEXT_FIELD_TAP_MS = 1_000L
|
||||
|
||||
@@ -18,10 +18,9 @@ object Library {
|
||||
val source: String = SOURCE_STEAM,
|
||||
val storeId: String? = null,
|
||||
val logo: File? = null,
|
||||
val installedAt: Long = 0L,
|
||||
) {
|
||||
fun withArt(art: File?, hero: File?, icon: File?, logo: File? = this.logo) =
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId, logo, installedAt)
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId, logo)
|
||||
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
val launchKey: String get() = if (storeId != null) "$source:$storeId" else gameIdString
|
||||
@@ -32,7 +31,6 @@ object Library {
|
||||
}
|
||||
}
|
||||
const val ADDED = "added"
|
||||
private const val PLAYED = "added_games_played"
|
||||
const val EPIC_LAUNCHER = "epic-launcher"
|
||||
const val EPIC_LAUNCHER_NAME = "Epic Games Launcher"
|
||||
const val BATTLENET_LAUNCHER = "battlenet-launcher"
|
||||
@@ -223,18 +221,6 @@ object Library {
|
||||
return (steam + own.map { g -> addedGame(context, g, roots, played[g.appId] ?: 0L) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun recordPlayed(context: Context, game: SteamGame) {
|
||||
if (game.library == ADDED) recordPlayed(context, game.gameIdString)
|
||||
}
|
||||
|
||||
fun recordPlayed(context: Context, gameId: String) {
|
||||
context.getSharedPreferences(PLAYED, Context.MODE_PRIVATE).edit()
|
||||
.putLong(gameId, System.currentTimeMillis() / 1000).apply()
|
||||
}
|
||||
|
||||
internal fun recordedPlay(context: Context, gameId: Long): Long =
|
||||
context.getSharedPreferences(PLAYED, Context.MODE_PRIVATE).getLong(java.lang.Long.toUnsignedString(gameId), 0L)
|
||||
|
||||
internal fun shortcutsLastPlayed(root: File): Map<Long, Long> = runCatching {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptyMap()
|
||||
val bytes = File(config, "shortcuts.vdf").takeIf { it.isFile }?.readBytes() ?: return emptyMap()
|
||||
@@ -263,13 +249,12 @@ object Library {
|
||||
AddedGameArt.resolve(context, g).let { art ->
|
||||
SteamGame(
|
||||
g.steamAppId ?: g.appId.toInt(), g.name, art.portrait ?: art.header, ADDED, g.gameId,
|
||||
hero = art.hero ?: art.header, lastPlayed = maxOf(lastPlayed, recordedPlay(context, g.gameId)), gameFiles = g.folder,
|
||||
hero = art.hero ?: art.header, lastPlayed = lastPlayed, gameFiles = g.folder,
|
||||
protonPrefix = if (g.source == com.droiddeck.launcher.stores.Store.BATTLENET.id) File(com.droiddeck.launcher.runtime.BattleNet.prefix(context), "pfx").takeIf { it.isDirectory }
|
||||
else protonPrefix(context, g.steamAppId?.toLong() ?: g.appId, libraries = roots),
|
||||
icon = art.icon?.takeIf { it.extension.lowercase() != "ico" },
|
||||
source = g.source, storeId = g.storeId,
|
||||
logo = art.logo,
|
||||
installedAt = g.folder.lastModified(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ object LibraryCache {
|
||||
source = o.optString("source").ifEmpty { if (o.getString("library") == Library.ADDED) Library.ADDED else Library.SOURCE_STEAM },
|
||||
storeId = o.optString("storeId").ifEmpty { null },
|
||||
logo = o.file("logo"),
|
||||
installedAt = o.optLong("installedAt"),
|
||||
)
|
||||
} to x64
|
||||
}.getOrElse { e -> Log.w(TAG, "unreadable, ignored: ${e.message}"); Pair(emptyList(), false) }
|
||||
@@ -45,7 +44,7 @@ object LibraryCache {
|
||||
JSONObject().put("appId", g.appId).put("name", g.name).put("library", g.library).put("gameId", g.gameId)
|
||||
.put("lastPlayed", g.lastPlayed).putFile("art", g.art).putFile("hero", g.hero)
|
||||
.putFile("gameFiles", g.gameFiles).putFile("protonPrefix", g.protonPrefix).putFile("icon", g.icon).putFile("logo", g.logo)
|
||||
.put("source", g.source).put("installedAt", g.installedAt).apply { g.storeId?.let { put("storeId", it) } },
|
||||
.put("source", g.source).apply { g.storeId?.let { put("storeId", it) } },
|
||||
)
|
||||
val f = file(context)
|
||||
val tmp = File(f.path + ".tmp")
|
||||
|
||||
@@ -159,12 +159,6 @@ object DriverBundle {
|
||||
SessionPrefs.setLinuxDriver(context, bundle.linuxId)
|
||||
}
|
||||
|
||||
fun selectDisplay(context: Context, id: String) {
|
||||
val left = active(context)
|
||||
SessionPrefs.setAndroidDriver(context, id)
|
||||
if (left != null && left.androidId != id) SessionPrefs.setLinuxDriver(context, "")
|
||||
}
|
||||
|
||||
fun remove(context: Context, bundle: Bundle) {
|
||||
TurnipDriver(context).remove(bundle.androidId)
|
||||
LinuxVulkanDriverManager(context).removeDriver(bundle.linuxId)
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
@@ -34,8 +33,6 @@ object BottomScreenGuests {
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal fun current(arrivedAt: Long, ownShownAt: Long?): Boolean = ownShownAt == null || arrivedAt >= ownShownAt
|
||||
|
||||
internal fun ignorable(type: Int): Boolean = type == AccessibilityWindowInfo.TYPE_INPUT_METHOD ||
|
||||
type == AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY ||
|
||||
type == AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER ||
|
||||
@@ -46,7 +43,7 @@ object BottomScreenGuests {
|
||||
}
|
||||
|
||||
class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
private class Arrival(val pkg: String, val at: Long)
|
||||
private class Arrival(val pkg: String)
|
||||
|
||||
private val thread = HandlerThread("bottom-screen").apply { start() }
|
||||
private val handler = Handler(thread.looper)
|
||||
@@ -55,8 +52,6 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
private var homes: Set<String> = emptySet()
|
||||
private var keyboards: Set<String> = emptySet()
|
||||
private var holders: Map<Int, String?> = emptyMap()
|
||||
private var ownWindows: Map<Int, Set<Int>> = emptyMap()
|
||||
private val ownShownAt = mutableMapOf<Int, Long>()
|
||||
private val check = Runnable { evaluate() }
|
||||
|
||||
fun start() {
|
||||
@@ -77,7 +72,7 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
|
||||
val pkg = event.packageName?.toString() ?: return
|
||||
if (event.displayId == Display.DEFAULT_DISPLAY || pkg == own || pkg == SYSTEM_UI) return
|
||||
arrivals[event.displayId] = Arrival(pkg, event.eventTime)
|
||||
arrivals[event.displayId] = Arrival(pkg)
|
||||
}
|
||||
AccessibilityEvent.TYPE_WINDOWS_CHANGED -> Unit
|
||||
else -> return
|
||||
@@ -102,25 +97,19 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
val all = runCatching { service.windowsOnAllDisplays }.getOrNull() ?: return
|
||||
val displays = service.getSystemService(DisplayManager::class.java) ?: return
|
||||
val taken = mutableMapOf<Int, String?>()
|
||||
val shown = mutableMapOf<Int, Set<Int>>()
|
||||
val now = SystemClock.uptimeMillis()
|
||||
for (i in 0 until all.size()) {
|
||||
val id = all.keyAt(i)
|
||||
if (id == Display.DEFAULT_DISPLAY) continue
|
||||
val display = displays.getDisplay(id)?.takeIf(DualScreen::builtIn) ?: continue
|
||||
val windows = all.valueAt(i)
|
||||
val topPkg = windows.asSequence()
|
||||
.filter { !BottomScreenGuests.ignorable(it.type) && cover(it, display) >= BottomScreenGuests.MIN_COVER }
|
||||
.sortedByDescending { it.layer }
|
||||
.map { it to packageOf(it) }
|
||||
val ours = windows.filter { it.second == own }.map { it.first.id }.toSet()
|
||||
if (!ownWindows[id].orEmpty().containsAll(ours)) ownShownAt[id] = now
|
||||
shown[id] = ours
|
||||
val topPkg = windows.firstNotNullOfOrNull { it.second?.takeUnless { pkg -> pkg == own } }
|
||||
val arrival = seen[id]?.takeIf { BottomScreenGuests.current(it.at, ownShownAt[id]) }
|
||||
seen[id]?.takeIf { topPkg != null || arrival == null }?.let { arrivals.remove(id, it) }
|
||||
.firstNotNullOfOrNull { packageOf(it)?.takeUnless { pkg -> pkg == own } }
|
||||
val arrival = seen[id]
|
||||
if (topPkg != null && arrival != null) arrivals.remove(id, arrival)
|
||||
BottomScreenGuests.holder(topPkg, arrival?.pkg?.takeUnless { it in keyboards }, homes)?.let { taken[id] = it }
|
||||
}
|
||||
ownWindows = shown
|
||||
if (current !== this) return
|
||||
if (taken != holders) Log.i(TAG, "bottom screen holders: $taken")
|
||||
holders = taken
|
||||
|
||||
@@ -73,7 +73,7 @@ object ControllerPrefs {
|
||||
keyboardButton = p.getBoolean("keyboardButton", true),
|
||||
selectSteam = p.getBoolean("selectSteam", false),
|
||||
joyConVr = joyConVr(context),
|
||||
snap = snap(context),
|
||||
snap = p.getBoolean("snap", true),
|
||||
customLayout = p.all.keys.any { it.startsWith("layout.") },
|
||||
mapping = mappable.associate { (id, _) -> id to target(context, id) },
|
||||
deadZone = deadZone(context),
|
||||
@@ -87,6 +87,11 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putString("map.$id", target).apply()
|
||||
}
|
||||
|
||||
fun resetMapping(context: Context) {
|
||||
val p = prefs(context)
|
||||
p.edit().apply { p.all.keys.filter { it.startsWith("map.") }.forEach { remove(it) } }.apply()
|
||||
}
|
||||
|
||||
fun setTint(context: Context, tint: Int) {
|
||||
prefs(context).edit().putInt("tint", tint).apply()
|
||||
}
|
||||
@@ -124,6 +129,10 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putBoolean("steamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setQamButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("qamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setKeyboardButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("keyboardButton", on).apply()
|
||||
}
|
||||
@@ -138,8 +147,6 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putBoolean("joyConVr", on).apply()
|
||||
}
|
||||
|
||||
fun snap(context: Context): Boolean = prefs(context).getBoolean("snap", true)
|
||||
|
||||
fun setSnap(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("snap", on).apply()
|
||||
}
|
||||
@@ -165,4 +172,9 @@ object ControllerPrefs {
|
||||
fun resetAll(context: Context) {
|
||||
prefs(context).edit().clear().putBoolean("rumble", true).apply()
|
||||
}
|
||||
|
||||
fun resetAllLayouts(context: Context) {
|
||||
val p = prefs(context)
|
||||
p.edit().apply { p.all.keys.filter { it.startsWith("layout.") }.forEach { remove(it) } }.apply()
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,6 @@ class DirectTouchHold(
|
||||
handler.removeCallbacks(hold)
|
||||
}
|
||||
|
||||
fun cancelTouch() {
|
||||
handler.removeCallbacks(hold)
|
||||
handler.removeCallbacks(lift)
|
||||
liftPending = false
|
||||
if (forwarded) touch.cancel()
|
||||
forwarded = false
|
||||
}
|
||||
|
||||
fun onTouch(event: MotionEvent): Boolean {
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
if (liftPending) {
|
||||
|
||||
@@ -4,13 +4,12 @@ import android.content.Context
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
|
||||
object InputLibraries {
|
||||
private const val VERSION = 14
|
||||
private const val VERSION = 12
|
||||
private const val KEY = "library"
|
||||
private const val RECOVERY = "library-recovery."
|
||||
const val OVERLAY = "builtin.overlay"
|
||||
@@ -31,8 +30,6 @@ object InputLibraries {
|
||||
if (version < 11) library = neutralStockLayouts(library)
|
||||
if (version < 10) library = restyleGamepads(context, library)
|
||||
if (version < 12) library = barStockLayouts(library)
|
||||
if (version < 13) library = LegacyScreenKeyMigration.pin(library)
|
||||
if (version < 14) library = deckGamepadSurfaces(context, library)
|
||||
if (version < 6 || !p.contains(LegacyScreenKeyMigration.BACKUP)) {
|
||||
val legacy = LegacyScreenKeyMigration.snapshot(context)
|
||||
if (legacy != null) {
|
||||
@@ -266,15 +263,6 @@ object InputLibraries {
|
||||
return addStockLayouts(result)
|
||||
}
|
||||
|
||||
private fun deckGamepadSurfaces(context: Context, library: InputLibrary): InputLibrary {
|
||||
val leftPad = SessionPrefs.gamepadSurface(context, right = false) == SessionPrefs.SURFACE_TRACKPAD
|
||||
val rightStick = SessionPrefs.gamepadSurface(context, right = true) == SessionPrefs.SURFACE_STICK
|
||||
if (!leftPad && !rightStick) return library
|
||||
val saved = library.layouts.firstOrNull { it.id == DeckPanel.GAMEPAD.layout } ?: return library
|
||||
if (saved.elements != InputStockLayouts.gamepad(leftPad = false, rightStick = false)) return library
|
||||
return library.withLayout(saved.copy(elements = InputStockLayouts.gamepad(leftPad, rightStick))).validate()
|
||||
}
|
||||
|
||||
private fun migrationBindingId(library: InputLibrary, binding: String, layout: String): String {
|
||||
val seed = JSONArray(listOf(binding, layout)).toString().toByteArray(Charsets.UTF_8)
|
||||
val base = "migrated.bindings." + UUID.nameUUIDFromBytes(seed)
|
||||
@@ -304,7 +292,7 @@ object InputLibraries {
|
||||
for (e in layout.elements) elements.put(JSONObject().put("id", e.id).put("kind", e.kind.name)
|
||||
.put("control", e.control.id).put("name", e.name).put("x", e.x.toDouble()).put("y", e.y.toDouble())
|
||||
.put("width", e.width.toDouble()).put("height", e.height.toDouble()).put("floating", e.floating).put("hideWhenIdle", e.hideWhenIdle).put("mouseGestures", e.mouseGestures).put("toggle", e.toggle).put("icon", e.icon).put("image", e.image)
|
||||
.put("elementOpacity", e.opacity.toDouble()).put("buttonShape", e.buttonShape.name).put("showLabel", e.showLabel).put("alwaysShown", e.alwaysShown))
|
||||
.put("elementOpacity", e.opacity.toDouble()).put("buttonShape", e.buttonShape.name).put("showLabel", e.showLabel))
|
||||
layouts.put(JSONObject().put("id", layout.id).put("name", layout.name).put("template", layout.template.name).put("elements", elements)
|
||||
.put("tint", layout.style.tint ?: JSONObject.NULL).put("opacity", layout.style.opacity).put("screen", layout.screen.name).put("aspect", layout.aspect.toDouble()).put("scale", layout.scale.toDouble())
|
||||
.put("hidden", JSONArray(layout.style.hidden.map { it.name }.sorted())).put("origin", layout.origin ?: JSONObject.NULL))
|
||||
@@ -345,7 +333,7 @@ object InputLibraries {
|
||||
e.getDouble("y").toFloat(), e.getDouble("width").toFloat(), e.getDouble("height").toFloat(),
|
||||
e.getString("id"), InputElementKind.valueOf(e.getString("kind")), e.getString("name"),
|
||||
e.optBoolean("floating", true), e.optBoolean("hideWhenIdle", false), e.optBoolean("mouseGestures", false), e.optBoolean("toggle", false), e.optInt("icon", 0), e.optString("image", ""),
|
||||
e.optDouble("elementOpacity", 1.0).toFloat(), InputButtonShape.valueOf(e.optString("buttonShape", InputButtonShape.ROUNDED.name)), e.optBoolean("showLabel", true), e.optBoolean("alwaysShown", false))
|
||||
e.optDouble("elementOpacity", 1.0).toFloat(), InputButtonShape.valueOf(e.optString("buttonShape", InputButtonShape.ROUNDED.name)), e.optBoolean("showLabel", true))
|
||||
}
|
||||
SavedInputLayout(layout.getString("id"), layout.getString("name"), elements, InputLayoutTemplate.valueOf(layout.getString("template")),
|
||||
InputSurfaceStyle(if (!layout.has("tint")) ControllerPrefs.STEAM_BLUE else if (layout.isNull("tint")) null else layout.getInt("tint"), layout.optInt("opacity", 100),
|
||||
|
||||
@@ -45,14 +45,6 @@ internal object InputStockLayouts {
|
||||
InputSurfaceElement(InputControl.SELECT, .42f, .3f, .07f, .1f, buttonShape = InputButtonShape.PILL),
|
||||
InputSurfaceElement(InputControl.START, .51f, .3f, .07f, .1f, buttonShape = InputButtonShape.PILL))
|
||||
}
|
||||
fun gamepad(leftPad: Boolean, rightStick: Boolean) = gamepad().map {
|
||||
when {
|
||||
leftPad && it.control == InputControl.LS -> InputSurfaceElement(InputControl.LEFT_PAD, it.x, it.y, it.width, it.height)
|
||||
rightStick && it.control == InputControl.RIGHT_PAD -> InputSurfaceElement(InputControl.RS, it.x, it.y, it.width, it.height)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
||||
fun layouts() = listOf(
|
||||
SavedInputLayout(InputLibraries.DECK, "Deck grips + trackpads", pads() + grips(), style = NEUTRAL, screen = InputLayoutScreen.SECONDARY),
|
||||
SavedInputLayout(InputLibraries.TRACKPAD, "Trackpad + keyboard", listOf(
|
||||
|
||||
@@ -34,7 +34,6 @@ data class InputSurfaceElement(
|
||||
val opacity: Float = 1f,
|
||||
val buttonShape: InputButtonShape = InputButtonShape.ROUNDED,
|
||||
val showLabel: Boolean = true,
|
||||
val alwaysShown: Boolean = false,
|
||||
) {
|
||||
val hasClick get() = kind == InputElementKind.STICK || kind == InputElementKind.TRACKPAD
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ class InputSurfaceSource(private val pad: PadBridge, private val role: InputRole
|
||||
private val gestureRight = if (element.mouseGestures) router?.channel(role, InputControl.A, fixedAction = InputAction.MouseButton(273)) else null
|
||||
private val gestureMiddle = if (element.mouseGestures) router?.channel(role, InputControl.A, fixedAction = InputAction.MouseButton(274)) else null
|
||||
val mouseGestures get() = element.mouseGestures && route?.action is InputAction.Pointer
|
||||
val directions get() = route?.action as? InputAction.Directions
|
||||
|
||||
fun gestureContact(touching: Boolean) { if (!closed) route?.update(0f, 0f, touching) }
|
||||
fun pointerDelta(dx: Float, dy: Float) {
|
||||
|
||||
@@ -225,8 +225,7 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
)
|
||||
}
|
||||
|
||||
fun dock(stored: List<InputSurfaceElement>, portrait: Boolean, grips: Boolean, left: InputControl = InputControl.LS,
|
||||
right: InputControl = InputControl.RS): List<InputSurfaceElement> {
|
||||
fun dock(stored: List<InputSurfaceElement>, portrait: Boolean, grips: Boolean): List<InputSurfaceElement> {
|
||||
val boxes = GamepadGeometry.layout(width.toFloat(), height.toFloat(), resources.displayMetrics.density, portrait)
|
||||
val placed = mutableListOf<InputSurfaceElement>()
|
||||
fun box(control: InputControl, slot: GamepadGeometry.Slot, shape: InputButtonShape) {
|
||||
@@ -244,9 +243,9 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
b.cx + direction.first * spread, b.cy + direction.second * spread, radius)
|
||||
}
|
||||
}
|
||||
box(left, GamepadGeometry.Slot.LEFT_MAIN, InputButtonShape.CIRCLE)
|
||||
box(InputControl.LS, GamepadGeometry.Slot.LEFT_MAIN, InputButtonShape.CIRCLE)
|
||||
cross(DPAD_ORDER, GamepadGeometry.Slot.LEFT_SECOND)
|
||||
box(right, GamepadGeometry.Slot.RIGHT_MAIN, InputButtonShape.CIRCLE)
|
||||
box(InputControl.RS, GamepadGeometry.Slot.RIGHT_MAIN, InputButtonShape.CIRCLE)
|
||||
cross(FACE_ORDER, GamepadGeometry.Slot.RIGHT_SECOND)
|
||||
box(InputControl.LT, GamepadGeometry.Slot.LEFT_OUTER_SHOULDER, InputButtonShape.PILL)
|
||||
box(InputControl.LB, GamepadGeometry.Slot.LEFT_INNER_SHOULDER, InputButtonShape.PILL)
|
||||
@@ -255,8 +254,7 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
box(InputControl.SELECT, GamepadGeometry.Slot.VIEW, InputButtonShape.PILL)
|
||||
box(InputControl.START, GamepadGeometry.Slot.MENU, InputButtonShape.PILL)
|
||||
if (grips) GRIPS.forEach { (control, slot) -> box(control, slot, InputButtonShape.ROUNDED) }
|
||||
val surfaces = listOf(left, right).filter { it.isTrackpad }
|
||||
return withGeometry(stored, placed) + placed.filter { p -> (GRIPS.any { it.first == p.control } || p.control in surfaces) && stored.none { it.id == p.id } }
|
||||
return withGeometry(stored, placed) + placed.filter { p -> GRIPS.any { it.first == p.control } && stored.none { it.id == p.id } }
|
||||
}
|
||||
|
||||
private fun circle(e: InputSurfaceElement, cx: Float, cy: Float, radius: Float) =
|
||||
|
||||
@@ -40,7 +40,7 @@ internal object LegacyScreenKeyMigration {
|
||||
if (result.profiles.any { it.id == id }) continue
|
||||
val converted = convert(context, set.second, metrics.widthPixels, metrics.heightPixels, metrics.density)
|
||||
if (converted.isEmpty()) continue
|
||||
val layout = SavedInputLayout("$id.layout", set.first, converted.map { it.first.copy(alwaysShown = true) }, style = style, screen = InputLayoutScreen.MAIN,
|
||||
val layout = SavedInputLayout("$id.layout", set.first, converted.map { it.first }, style = style, screen = InputLayoutScreen.MAIN,
|
||||
aspect = (metrics.widthPixels.coerceAtLeast(1).toFloat() / metrics.heightPixels.coerceAtLeast(1)).coerceIn(.05f, 20f))
|
||||
val bindings = SavedInputBindings("$id.bindings", set.first, false, layout.id, elements = converted.associate { it.first.id to it.second })
|
||||
val base = library.profile(library.steam)
|
||||
@@ -68,17 +68,6 @@ internal object LegacyScreenKeyMigration {
|
||||
return result.validate()
|
||||
}
|
||||
|
||||
fun pin(library: InputLibrary): InputLibrary {
|
||||
val keys = library.layouts.firstOrNull { it.id == "$SHOWN.layout" }?.elements.orEmpty()
|
||||
return library.copy(layouts = library.layouts.map { layout ->
|
||||
val imported = layout.id.startsWith("migrated.screenkeys.")
|
||||
layout.copy(elements = layout.elements.map { e ->
|
||||
val key = e.id.startsWith("imported:") && (imported || keys.any { it.id == e.id && it.kind == e.kind && it.name == e.name })
|
||||
if (key) e.copy(alwaysShown = true) else e
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
internal fun convert(context: Context, raw: String, width: Int, height: Int, density: Float): List<Pair<InputSurfaceElement, InputAction>> =
|
||||
convert(context, parse(raw), width, height, density)
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
private set
|
||||
var isQuickHidden = false
|
||||
private set
|
||||
var isPinnedOnly = false
|
||||
private set
|
||||
val hasPinned get() = layout?.elements?.any { it.alwaysShown } == true
|
||||
var onTouchMiss: ((MotionEvent) -> Unit)?
|
||||
get() = surface.onTouchMiss
|
||||
set(value) { surface.onTouchMiss = value }
|
||||
@@ -64,13 +61,6 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
render()
|
||||
}
|
||||
|
||||
fun setPinnedOnly(enabled: Boolean) {
|
||||
if (isPinnedOnly == enabled) return
|
||||
releaseAll()
|
||||
isPinnedOnly = enabled
|
||||
render()
|
||||
}
|
||||
|
||||
private data class Placement(val layout: SavedInputLayout?, val width: Int, val height: Int, val safe: Rect, val buttonsOnly: Boolean)
|
||||
|
||||
private fun placed(): List<InputSurfaceElement> {
|
||||
@@ -82,16 +72,12 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
|
||||
private fun render() {
|
||||
val elements = placed()
|
||||
surface.updateCustomLayout(when {
|
||||
isPinnedOnly -> elements.filter { it.alwaysShown }
|
||||
isQuickHidden -> elements.filter { it.id == "builtin:keyboard" || it.alwaysShown }
|
||||
else -> elements
|
||||
})
|
||||
surface.updateCustomLayout(if (isQuickHidden) elements.filter { it.id == "builtin:keyboard" } else elements)
|
||||
val style = layout?.style ?: InputSurfaceStyle()
|
||||
surface.updateStyle(style)
|
||||
quickToggle.tint = style.tint
|
||||
quickToggle.alpha = style.opacity / 100f
|
||||
quickToggle.visibility = if (isButtonsOnly || isPinnedOnly || layout == null || dock != null) GONE else VISIBLE
|
||||
quickToggle.visibility = if (isButtonsOnly || layout == null || dock != null) GONE else VISIBLE
|
||||
quickToggle.invalidate()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,7 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object OnScreenLayout {
|
||||
data class Dock(val portrait: Boolean, val grips: Boolean, val left: InputControl = InputControl.LS, val right: InputControl = InputControl.RS) {
|
||||
companion object {
|
||||
fun deck(portrait: Boolean, gamepad: SavedInputLayout?): Dock {
|
||||
fun side(right: Boolean) = gamepad?.elements?.firstOrNull {
|
||||
(it.kind == InputElementKind.STICK || it.kind == InputElementKind.TRACKPAD) && (it.x + it.width / 2f >= .5f) == right
|
||||
}?.kind
|
||||
val left = if (side(false) == InputElementKind.TRACKPAD) InputControl.LEFT_PAD else InputControl.LS
|
||||
val right = if (side(true) == InputElementKind.STICK) InputControl.RS else InputControl.RIGHT_PAD
|
||||
return Dock(portrait, true, left, right)
|
||||
}
|
||||
}
|
||||
}
|
||||
data class Dock(val portrait: Boolean, val grips: Boolean)
|
||||
|
||||
private const val TOLERANCE = .01f
|
||||
private const val KEYBOARD_ID = "builtin:keyboard"
|
||||
@@ -28,13 +17,13 @@ object OnScreenLayout {
|
||||
val stored = layout?.elements ?: return emptyList()
|
||||
if (width <= 0 || height <= 0) return stored
|
||||
val generator = LegacyGamepadLayout(context, width, height, safe, layout.scale)
|
||||
if (buttonsOnly && dock == null) return generator.buttonsOnly(stored)
|
||||
val (reference, generated) = reference(context, layout, width, height)
|
||||
if (buttonsOnly && dock == null) return generator.buttonsOnly(stored) + fit(stored.filter { it.alwaysShown }, reference.first, reference.second, width, height, safe)
|
||||
val gamepad = if (generated) stored.filter(::isGamepad) else emptyList()
|
||||
val others = fit(stored.filterNot { generated && isGamepad(it) || dock != null && it.alwaysShown }, reference.first, reference.second, width, height, safe)
|
||||
val others = fit(stored.filterNot { generated && isGamepad(it) }, reference.first, reference.second, width, height, safe)
|
||||
val placed = when {
|
||||
gamepad.isEmpty() -> emptyList()
|
||||
dock != null -> generator.dock(gamepad, dock.portrait, dock.grips, dock.left, dock.right)
|
||||
dock != null -> generator.dock(gamepad, dock.portrait, dock.grips)
|
||||
else -> generator.regenerate(gamepad)
|
||||
}
|
||||
val byId = (placed + others).associateBy { it.id }
|
||||
|
||||
@@ -52,7 +52,4 @@ object SecondScreenDisplays {
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
fun editorCandidates(selected: Int, available: List<Int>, taken: (Int) -> Boolean): List<Int> =
|
||||
(listOf(selected) + available).distinct().filterNot(taken)
|
||||
}
|
||||
@@ -82,12 +82,8 @@ public final class LinuxRuntimeInstaller {
|
||||
private LinuxRuntimeInstaller() {}
|
||||
|
||||
public static String installedVersion(Context context) {
|
||||
return LinuxRuntime.isInstalled(context) ? rootfsVersion(context) : null;
|
||||
}
|
||||
|
||||
private static String rootfsVersion(Context context) {
|
||||
File marker = new File(LinuxRuntime.rootDir(context), VERSION_FILE);
|
||||
if (!marker.isFile()) return null;
|
||||
if (!marker.isFile() || !LinuxRuntime.isInstalled(context)) return null;
|
||||
String v = FileUtils.readString(marker);
|
||||
return v == null ? null : v.trim();
|
||||
}
|
||||
@@ -148,10 +144,6 @@ public final class LinuxRuntimeInstaller {
|
||||
}
|
||||
};
|
||||
boolean desktop = DesktopCatalog.INSTANCE.desktopInstalled(app);
|
||||
if (packsOnly(app, release, progress)) {
|
||||
job.ok = true;
|
||||
return true;
|
||||
}
|
||||
job.ok = installOnce(app, release, progress);
|
||||
if (job.ok && desktop) keepDesktop(app, progress);
|
||||
return job.ok;
|
||||
@@ -291,31 +283,6 @@ public final class LinuxRuntimeInstaller {
|
||||
for (File file : files) file.delete();
|
||||
}
|
||||
|
||||
private static kotlin.jvm.functions.Function1<AddonPack.State, kotlin.Unit> packProgress(ProgressListener listener, String stage) {
|
||||
return state -> {
|
||||
if (listener != null && state instanceof AddonPack.State.Downloading) {
|
||||
listener.onProgress(Step.DOWNLOADING, stage, Math.round(((AddonPack.State.Downloading) state).getProgress() * 100f));
|
||||
}
|
||||
return kotlin.Unit.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean packsOnly(Context context, Release release, ProgressListener listener) {
|
||||
if (!LinuxRuntime.hasRootfs(context) || LinuxRuntime.isInstalled(context) || !release.version.equals(rootfsVersion(context))) return false;
|
||||
File root = LinuxRuntime.rootDir(context);
|
||||
try {
|
||||
recoverInterruptedSwap(root, new File(root.getParentFile(), LinuxRuntime.DIR + ".new"), new File(root.getParentFile(), LinuxRuntime.DIR + ".old"));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "packs only: " + e);
|
||||
return false;
|
||||
}
|
||||
String downloading = context.getString(R.string.rtinst_downloading);
|
||||
if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0);
|
||||
return AddonPack.PROOT.await(context, () -> true, packProgress(listener, downloading))
|
||||
&& AddonPack.DIRECTAUDIO.await(context, () -> true, packProgress(listener, downloading))
|
||||
&& LinuxRuntime.isInstalled(context);
|
||||
}
|
||||
|
||||
private static boolean installOnce(Context context, Release release, ProgressListener listener) {
|
||||
File archive = new File(context.getCacheDir(), archiveName(release));
|
||||
boolean keepPartial = false;
|
||||
@@ -327,10 +294,10 @@ public final class LinuxRuntimeInstaller {
|
||||
emptyTrash(LinuxRuntime.rootDir(context).getParentFile());
|
||||
String downloading = context.getString(R.string.rtinst_downloading);
|
||||
if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0);
|
||||
if (!LinuxRuntime.patchedProot(context) && !AddonPack.PROOT.await(context, () -> true, packProgress(listener, downloading))
|
||||
if (!LinuxRuntime.patchedProot(context) && !AddonPack.PROOT.await(context)
|
||||
&& !SessionPrefs.allowStockProot(context))
|
||||
throw new IOException("Cannot download proot");
|
||||
if (!AddonPack.DIRECTAUDIO.await(context, () -> true, packProgress(listener, downloading))) throw new IOException("Cannot download DirectAudio");
|
||||
if (!AddonPack.DIRECTAUDIO.await(context)) throw new IOException("Cannot download DirectAudio");
|
||||
boolean ok = Downloader.downloadFile(release.url, archive, true, release.sha256, (fraction) -> {
|
||||
if (listener != null) {
|
||||
listener.onProgress(Step.DOWNLOADING, downloading,
|
||||
|
||||
@@ -68,9 +68,8 @@ object GameStorage {
|
||||
externalStoragePath: Boolean = isExternalStoragePath(path),
|
||||
): Boolean = when {
|
||||
state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY -> true
|
||||
path.isDirectory -> true
|
||||
externalStoragePath -> false
|
||||
state == Environment.MEDIA_UNKNOWN || state == null -> path.parentFile?.isDirectory == true
|
||||
state == Environment.MEDIA_UNKNOWN || state == null -> path.isDirectory || path.parentFile?.isDirectory == true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
@@ -1555,6 +1555,10 @@ object SessionPrefs {
|
||||
prefs(context).getString(if (right) "gamepadRight" else "gamepadLeft", null)
|
||||
?: if (right) SURFACE_TRACKPAD else SURFACE_STICK
|
||||
|
||||
fun setGamepadSurface(context: Context, right: Boolean, surface: String) {
|
||||
prefs(context).edit().putString(if (right) "gamepadRight" else "gamepadLeft", surface).apply()
|
||||
}
|
||||
|
||||
fun deckPanel(context: Context): com.droiddeck.launcher.input.DeckPanel =
|
||||
com.droiddeck.launcher.input.DeckPanel.from(prefs(context).getString("deckPanel", null))
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@ private fun releaseDateValue(raw: String): Long =
|
||||
runCatching { java.time.LocalDate.parse(day).toEpochDay() }.getOrDefault(0L)
|
||||
} ?: 0L
|
||||
|
||||
internal fun entryTime(first: Long?, fallback: Long?): Long = first?.takeIf { it > 0 } ?: fallback?.takeIf { it > 0 } ?: 0L
|
||||
|
||||
fun <T> sortGames(
|
||||
items: List<T>,
|
||||
sort: String,
|
||||
|
||||
@@ -204,8 +204,7 @@ object BattleNetBackend : StoreBackend {
|
||||
fun installedGames(context: Context): List<InstalledStoreGame> = installed(context).map { game ->
|
||||
val art = BattleNet.art(context, game.title)
|
||||
val cover = art.cover?.let { Uri.fromFile(it).toString() }
|
||||
InstalledStoreGame(StoreGameSidecar(store, game.title.uid, game.name, "", installedAt = game.folder.lastModified(),
|
||||
cover = cover, hero = art.header?.let { Uri.fromFile(it).toString() } ?: cover), game.folder)
|
||||
InstalledStoreGame(StoreGameSidecar(store, game.title.uid, game.name, "", cover = cover, hero = art.header?.let { Uri.fromFile(it).toString() } ?: cover), game.folder)
|
||||
}
|
||||
|
||||
private fun shelves(context: Context): StoreShelves {
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.droiddeck.launcher.input.*
|
||||
internal fun InputActionEditor(
|
||||
control: InputControl, current: InputAction, title: String, onBack: () -> Unit, onSave: (InputAction) -> Unit,
|
||||
element: InputSurfaceElement? = null, nativeTargets: Boolean = true, detail: String? = null,
|
||||
properties: (@Composable ColumnScope.(InputAction) -> Unit)? = null,
|
||||
properties: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
click: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -46,7 +46,7 @@ internal fun InputActionEditor(
|
||||
SettingsPage(host, title, onBack, eyebrow = stringResource(R.string.input_properties), lede = detail,
|
||||
action = { PrimaryButton(stringResource(R.string.gesture_save_changes), enabled = valid(draft),
|
||||
modifier = Modifier.testTag("input-action-save")) { onSave(draft) } }) {
|
||||
properties?.invoke(this, draft)
|
||||
properties?.invoke(this)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SheetChips(listOf(stringResource(R.string.input_route_controller), stringResource(R.string.input_route_local), stringResource(R.string.input_disabled)), route) {
|
||||
when (it) {
|
||||
|
||||
@@ -58,17 +58,12 @@ internal fun InputDisplayContent(host: ComponentActivity, display: Display, onDi
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
(host as? com.droiddeck.launcher.SessionActivity)?.inputWindowShown(display.displayId)
|
||||
showPresentation()
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP && !host.screenIsOff()) presentation.hide()
|
||||
if (event == Lifecycle.Event.ON_START) showPresentation()
|
||||
}
|
||||
host.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
host.lifecycle.removeObserver(observer)
|
||||
presentation.dismiss()
|
||||
(host as? com.droiddeck.launcher.SessionActivity)?.inputWindowClosed(display.displayId)
|
||||
}
|
||||
onDispose { host.lifecycle.removeObserver(observer); presentation.dismiss() }
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
internal fun InputElementProperties(element: InputSurfaceElement, action: InputAction? = null, onChange: (InputSurfaceElement) -> Unit) {
|
||||
internal fun InputElementProperties(element: InputSurfaceElement, onChange: (InputSurfaceElement) -> Unit) {
|
||||
val current by rememberUpdatedState(element)
|
||||
val change by rememberUpdatedState(onChange)
|
||||
OutlinedTextField(element.name, { onChange(element.copy(name = it.take(80))) }, label = { Text(stringResource(R.string.input_name)) },
|
||||
@@ -72,9 +72,7 @@ internal fun InputElementProperties(element: InputSurfaceElement, action: InputA
|
||||
}
|
||||
val host = rememberMenuHost()
|
||||
if ((element.kind == InputElementKind.BUTTON || element.kind == InputElementKind.TRIGGER) && element.icon == 0 && element.image.isEmpty() ||
|
||||
element.kind == InputElementKind.STICK && element.id.startsWith("legacy:") ||
|
||||
(element.kind == InputElementKind.STICK || element.kind == InputElementKind.DPAD) && (action as? InputAction.Directions)
|
||||
?.let { listOf(it.up, it.right, it.down, it.left).plus(if (element.kind == InputElementKind.STICK) it.diagonals.orEmpty() else emptyList()).any { code -> code != 0 } } == true)
|
||||
element.kind == InputElementKind.STICK && element.id.startsWith("legacy:"))
|
||||
ToggleRow(host, "element-label", stringResource(R.string.input_show_label), null, element.showLabel) { onChange(element.copy(showLabel = it)) }
|
||||
SliderRow(stringResource(R.string.ctrl_opacity), null, (element.opacity * 100).roundToInt().coerceIn(20, 100), 20..100, step = 5,
|
||||
format = { "$it%" }, modifier = Modifier.testTag("setting-element-opacity")) { onChange(element.copy(opacity = it / 100f)) }
|
||||
|
||||
@@ -169,7 +169,7 @@ private fun InputSettingsContent(onBack: () -> Unit, onChanged: () -> Unit, embe
|
||||
?: b.controls[editing.control] ?: InputAction.Native()
|
||||
InputActionEditor(editing.control, current, if (editing.click) stringResource(R.string.input_click_name, editing.element!!.name)
|
||||
else editing.element?.name ?: editing.key?.label ?: editing.control.label,
|
||||
properties = { action -> if (!editing.click) elementProperties?.let { e -> InputElementProperties(e, action) { elementProperties = it } } },
|
||||
properties = { if (!editing.click) elementProperties?.let { e -> InputElementProperties(e) { elementProperties = it } } },
|
||||
onBack = { edit = null }, element = editing.element, nativeTargets = editing.key == null, click = editing.click,
|
||||
detail = if (library.usersOfBindings(b.id).size > 1) stringResource(R.string.input_shared_detail, b.name, library.usersOfBindings(b.id).size)
|
||||
else "${profile.name} · ${roleLabel(role)}", onSave = { action ->
|
||||
|
||||
@@ -69,7 +69,7 @@ private class GamepadScaler(private val context: android.content.Context, privat
|
||||
}
|
||||
|
||||
private class SurfaceDraft(val layout: SavedInputLayout, val preset: SavedInputBindings, val defaults: List<InputSurfaceElement>? = null,
|
||||
private val gamepad: GamepadScaler? = null, snap: Boolean = true) {
|
||||
private val gamepad: GamepadScaler? = null) {
|
||||
var elements by mutableStateOf(layout.elements)
|
||||
var actions by mutableStateOf(preset.elements)
|
||||
var clicks by mutableStateOf(preset.clicks)
|
||||
@@ -77,7 +77,7 @@ private class SurfaceDraft(val layout: SavedInputLayout, val preset: SavedInputB
|
||||
var toolsExpanded by mutableStateOf(false)
|
||||
var toolsX by mutableFloatStateOf(.02f)
|
||||
var toolsY by mutableFloatStateOf(.02f)
|
||||
var snap by mutableStateOf(snap)
|
||||
var snap by mutableStateOf(true)
|
||||
var adding by mutableStateOf(false)
|
||||
var properties by mutableStateOf(false)
|
||||
var clickProperties by mutableStateOf(false)
|
||||
@@ -132,7 +132,7 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val (w, h) = OnScreenLayout.generatedSize(context, savedLayout, metrics.widthPixels, metrics.heightPixels) ?: (metrics.widthPixels to metrics.heightPixels)
|
||||
GamepadScaler(display, w, h)
|
||||
}
|
||||
SurfaceDraft(savedLayout, savedBindings, InputLibraries.defaultPositions(context, library, savedLayout), gamepad, ControllerPrefs.snap(context))
|
||||
SurfaceDraft(savedLayout, savedBindings, InputLibraries.defaultPositions(context, library, savedLayout), gamepad)
|
||||
}
|
||||
val manager = remember { context.getSystemService(DisplayManager::class.java) }
|
||||
var revision by remember { mutableIntStateOf(0) }
|
||||
@@ -162,7 +162,7 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val listener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(id: Int) { revision++ }
|
||||
override fun onDisplayRemoved(id: Int) { revision++ }
|
||||
override fun onDisplayChanged(id: Int) = Unit
|
||||
override fun onDisplayChanged(id: Int) { revision++ }
|
||||
}
|
||||
manager.registerDisplayListener(listener, Handler(Looper.getMainLooper()))
|
||||
onDispose { manager.unregisterDisplayListener(listener) }
|
||||
@@ -171,18 +171,13 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val presentation = SurfaceEditorPresentation(host, target, transparent = host is SessionActivity && role == InputRole.PRIMARY) {
|
||||
SurfaceCanvas(draft, transparent = host is SessionActivity && role == InputRole.PRIMARY)
|
||||
}
|
||||
(host as? SessionActivity)?.inputWindowShown(target.displayId)
|
||||
runCatching { presentation.show() }.onFailure { failure = it.message }
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP && !host.screenIsOff()) presentation.hide()
|
||||
if (event == Lifecycle.Event.ON_START) runCatching { presentation.show() }
|
||||
}
|
||||
host.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
host.lifecycle.removeObserver(observer)
|
||||
presentation.dismiss()
|
||||
(host as? SessionActivity)?.inputWindowClosed(target.displayId)
|
||||
}
|
||||
onDispose { host.lifecycle.removeObserver(observer); presentation.dismiss() }
|
||||
}
|
||||
// Compose 1.6 custom dialog sizing subtracts system bars even when they are hidden.
|
||||
Dialog(onDismissRequest = ::leave, properties = DialogProperties(usePlatformDefaultWidth = true, decorFitsSystemWindows = false)) {
|
||||
@@ -203,8 +198,8 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
if (draft.clickProperties) draft.clickAction(element) else draft.action(element),
|
||||
if (draft.clickProperties) stringResource(R.string.input_click_name, element.name) else element.name,
|
||||
onBack = { draft.properties = false }, element = element, click = draft.clickProperties,
|
||||
properties = { action ->
|
||||
if (!draft.clickProperties) InputElementProperties(properties, action) { properties = it }
|
||||
properties = {
|
||||
if (!draft.clickProperties) InputElementProperties(properties) { properties = it }
|
||||
if (layoutUsers > 1) Text(stringResource(R.string.input_editor_shared, layoutUsers),
|
||||
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
@@ -314,14 +309,13 @@ private fun FloatingSurfaceTools(draft: SurfaceDraft, onSave: () -> Unit, onLeav
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SurfaceToolbar(draft: SurfaceDraft, onSave: () -> Unit, onLeave: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
FlowRow(Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
SecondaryButton(stringResource(R.string.common_cancel), compact = true, onClick = onLeave)
|
||||
SecondaryButton(stringResource(R.string.input_editor_add), enabled = draft.elements.size < 64, compact = true) { draft.adding = true }
|
||||
SecondaryButton(stringResource(R.string.input_editor_undo), enabled = draft.history.isNotEmpty(), compact = true) { draft.undo() }
|
||||
if (draft.defaults != null) SecondaryButton(stringResource(R.string.input_reset_positions), enabled = draft.positioned != null, compact = true) { draft.resetPositions() }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(draft.snap, { draft.snap = it; ControllerPrefs.setSnap(context, it) })
|
||||
Checkbox(draft.snap, { draft.snap = it })
|
||||
Text(stringResource(R.string.ctrl_editor_snap))
|
||||
}
|
||||
PrimaryButton(stringResource(R.string.input_editor_done), compact = true, onClick = onSave)
|
||||
|
||||
@@ -32,12 +32,10 @@ import com.droiddeck.launcher.input.InputSurfaceStyle
|
||||
import com.droiddeck.launcher.input.pointerSubset
|
||||
import com.droiddeck.launcher.input.InputSurfaceSource
|
||||
import com.droiddeck.launcher.input.InputElementKind
|
||||
import com.droiddeck.launcher.input.InputAction
|
||||
import com.droiddeck.launcher.input.InputButtonIcons
|
||||
import com.droiddeck.launcher.input.InputButtonShape
|
||||
import com.droiddeck.launcher.input.InputButtonImages
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.pcKeyLabel
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
@@ -197,7 +195,7 @@ class InputSurfaceView(
|
||||
canvas.addView(when (element.kind) {
|
||||
InputElementKind.STICK -> StickView(context, element, source, element.floating, element.hideWhenIdle && pad != null)
|
||||
InputElementKind.TRACKPAD -> TrackpadView(context, element.control == InputControl.RIGHT_PAD, source = source)
|
||||
InputElementKind.DPAD -> DirectionPad(context, element, source)
|
||||
InputElementKind.DPAD -> DirectionPad(context, source)
|
||||
else -> HoldButton(context, element, source)
|
||||
}.apply { tag = element; contentDescription = element.name; styleElement(this); source?.onStateChanged = { postInvalidate() } })
|
||||
}
|
||||
@@ -543,7 +541,6 @@ class InputSurfaceView(
|
||||
if (touching || clicking) shade(color, 160, 1f) else shade(color, 120, 0.35f), if (touching) light(color, 230, 0.7f) else light(color, 150, 0.25f))
|
||||
if (!element.showLabel) return
|
||||
labelPaint.color = textColor(touching)
|
||||
if (drawDirectionLabels(canvas, source, false, cx, cy, radius * 0.66f, radius * 0.34f, labelPaint)) return
|
||||
labelPaint.textSize = knobRadius * 0.8f
|
||||
val label = if (element.name == element.control.label) glyph(element.control) else element.name
|
||||
canvas.drawText(label, cx + knobX, cy + knobY + labelPaint.textSize * 0.35f, labelPaint)
|
||||
@@ -555,9 +552,6 @@ class InputSurfaceView(
|
||||
canvas.drawCircle(cx, cy, reach(), ring)
|
||||
knob.color = if (touching) ACCENT else KNOB
|
||||
canvas.drawCircle(cx + knobX, cy + knobY, reach() * 0.45f, knob)
|
||||
if (!element.showLabel) return
|
||||
labelPaint.color = if (touching) ACCENT else TEXT
|
||||
drawDirectionLabels(canvas, source, false, cx, cy, reach() * 1.32f, reach() * 0.68f, labelPaint)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -619,11 +613,10 @@ class InputSurfaceView(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DirectionPad(context: Context, private val element: InputSurfaceElement, private val source: InputSurfaceSource? = null) : View(context) {
|
||||
private inner class DirectionPad(context: Context, private val source: InputSurfaceSource? = null) : View(context) {
|
||||
private val density = resources.displayMetrics.density
|
||||
private var held = 0
|
||||
private val arm = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val label = Paint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER; typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD); color = TEXT }
|
||||
private val rect = RectF()
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
@@ -643,7 +636,6 @@ class InputSurfaceView(
|
||||
armRect(2, cx + thick, cy - thick, cx + half, cy + thick)
|
||||
arm.color = KNOB
|
||||
canvas.drawRect(cx - thick, cy - thick, cx + thick, cy + thick, arm)
|
||||
if (element.showLabel) drawDirectionLabels(canvas, source, true, cx, cy, (half + thick) / 2f, thick * 1.7f, label)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -725,29 +717,6 @@ class InputSurfaceView(
|
||||
else -> control.id.uppercase()
|
||||
}
|
||||
|
||||
private val CARDINAL = listOf(0f to -1f, 1f to 0f, 0f to 1f, -1f to 0f)
|
||||
private val DIAGONAL = listOf(.7071f to -.7071f, .7071f to .7071f, -.7071f to .7071f, -.7071f to -.7071f)
|
||||
|
||||
internal fun directionLabels(context: Context, action: InputAction.Directions?, cardinalOnly: Boolean): List<Pair<Pair<Float, Float>, String>> {
|
||||
if (action == null) return emptyList()
|
||||
val codes = listOf(action.up, action.right, action.down, action.left).zip(CARDINAL) +
|
||||
if (cardinalOnly) emptyList() else action.diagonals.orEmpty().zip(DIAGONAL)
|
||||
return codes.filter { it.first != 0 }.map { (code, unit) -> unit to pcKeyLabel(context, code) }
|
||||
}
|
||||
|
||||
private fun View.drawDirectionLabels(canvas: Canvas, source: InputSurfaceSource?, cardinalOnly: Boolean, cx: Float, cy: Float,
|
||||
distance: Float, box: Float, paint: Paint): Boolean {
|
||||
val labels = directionLabels(context, source?.directions, cardinalOnly)
|
||||
paint.style = Paint.Style.FILL
|
||||
for ((unit, text) in labels) {
|
||||
paint.textSize = box * .6f
|
||||
val measured = paint.measureText(text)
|
||||
if (measured > box) paint.textSize *= box / measured
|
||||
canvas.drawText(text, cx + unit.first * distance, cy + unit.second * distance - (paint.ascent() + paint.descent()) / 2f, paint)
|
||||
}
|
||||
return labels.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun drawArrow(canvas: Canvas, x: Float, y: Float, size: Float, direction: Int, paint: Paint) {
|
||||
val (fx, fy) = when (direction) { 0 -> 0f to -1f; 1 -> 1f to 0f; 2 -> 0f to 1f; else -> -1f to 0f }
|
||||
val path = Path()
|
||||
|
||||
@@ -109,7 +109,6 @@ import com.droiddeck.launcher.stores.itch.ItchUpdates
|
||||
import com.droiddeck.launcher.stores.gog.GogMaintenance
|
||||
import com.droiddeck.launcher.stores.retainOrder
|
||||
import com.droiddeck.launcher.stores.xbox.XboxLibraryFilter
|
||||
import com.droiddeck.launcher.stores.entryTime
|
||||
import com.droiddeck.launcher.stores.sortGames
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -297,8 +296,8 @@ internal fun LibraryPage(s: FrontEndState, a: FrontEndActions, selected: String,
|
||||
val sizesPending = sizeSort && !sizesWaited && candidates.any { sizeOf(it) == null }
|
||||
val sorted = sortGames(
|
||||
candidates, sort, title = { it.title }, releaseDate = { it.item?.releaseDate.orEmpty() },
|
||||
lastPlayed = { e -> entryTime(e.game?.lastPlayed, e.storeKey?.let { lastPlayed[it] }) },
|
||||
installedAt = { e -> entryTime(e.storeKey?.let { installedAt[it] }, e.game?.installedAt) },
|
||||
lastPlayed = { e -> e.game?.lastPlayed ?: e.storeKey?.let { lastPlayed[it] } ?: 0L },
|
||||
installedAt = { e -> e.storeKey?.let { installedAt[it] } ?: 0L },
|
||||
size = { e -> sizeOf(e) ?: 0L },
|
||||
)
|
||||
var stable by remember { mutableStateOf(emptyList<LibraryEntry>()) }
|
||||
|
||||
@@ -323,7 +323,7 @@ class SecondScreenPresentation(
|
||||
context.getString(if (keyboardShown.value) R.string.second_hide_keyboard else R.string.second_show_keyboard),
|
||||
icon = R.drawable.ic_bottom_keyboard, active = keyboardShown.value, tag = "bottom-keyboard") { showKeyboard() }))
|
||||
SecondScreenMode.DECK_CONTROLS -> model(swap, deckPanelTabs(context, deckPanel.value, ::selectDeckPanel))
|
||||
SecondScreenMode.DASHBOARD, SecondScreenMode.CUSTOM_CONTROLS -> model(swap)
|
||||
SecondScreenMode.DASHBOARD -> model(swap)
|
||||
SecondScreenMode.TERMINAL -> model(listOf(BarExtra(context.getString(R.string.drawer_keyboard), icon = R.drawable.ic_bottom_keyboard,
|
||||
tag = "bottom-keyboard") { terminalPanel?.showKeyboard() }))
|
||||
else -> null
|
||||
|
||||
@@ -219,6 +219,7 @@ class DrawerActions(
|
||||
val frameGen: FrameGen.Mode,
|
||||
val lossless: Lossless.State,
|
||||
val oscMode: String,
|
||||
val onScreenButtonsVisible: Boolean,
|
||||
val suspendPolicy: String,
|
||||
val steamDownloadsInBackground: Boolean = false,
|
||||
val micAvailable: Boolean = false,
|
||||
@@ -324,7 +325,6 @@ class DrawerActions(
|
||||
val onStop: () -> Unit,
|
||||
val onStopFrom: ((androidx.compose.ui.geometry.Rect?) -> Unit)? = null,
|
||||
val onClose: () -> Unit,
|
||||
val onDismissScreen: (() -> Unit)? = null,
|
||||
val components: ComponentsManager.Snapshot? = null,
|
||||
val systemPowerVr: Boolean = false,
|
||||
val onComponentsRefresh: () -> Unit = {},
|
||||
@@ -488,7 +488,7 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
) {
|
||||
SteamActionPill(
|
||||
stringResource(R.string.common_close), Icons.Outlined.Close, focus.track(page, "close"),
|
||||
) { host.open = null; (a.onDismissScreen ?: a.onClose)() }
|
||||
) { host.open = null; a.onClose() }
|
||||
if (roomyHeader) Spacer(Modifier.weight(1f))
|
||||
SteamActionPill(
|
||||
stringResource(R.string.stores_dl_pause), Icons.Outlined.Pause, focus.track(page, "pause"),
|
||||
|
||||
@@ -1259,6 +1259,7 @@
|
||||
<string name="deck_tab_gamepad">يد التحكّم</string>
|
||||
<string name="deck_surface_stick">عصا التحكّم</string>
|
||||
<string name="deck_surface_trackpad">لوحة اللمس</string>
|
||||
<string name="deck_surface_switch">التبديل بين عصا التحكّم ولوحة اللمس</string>
|
||||
<string name="deck_close">إغلاق عناصر التحكّم على الشاشة الثانية</string>
|
||||
<string name="deck_open_steam_menu">فتح قائمة Steam</string>
|
||||
<string name="deck_right_trackpad">لوحة اللمس اليمنى</string>
|
||||
|
||||
@@ -1308,6 +1308,7 @@
|
||||
<string name="deck_tab_gamepad">Control</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Cambiar entre stick y trackpad</string>
|
||||
<string name="deck_close">Cerrar controles de la segunda pantalla</string>
|
||||
<string name="deck_open_steam_menu">Abrir menú de Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad derecho</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стик</string>
|
||||
<string name="deck_surface_trackpad">Тракпад</string>
|
||||
<string name="deck_surface_switch">Превключване между стик и тракпад</string>
|
||||
<string name="deck_close">Затваряне на контролите за втория екран</string>
|
||||
<string name="deck_open_steam_menu">Отваряне на менюто на Steam</string>
|
||||
<string name="deck_right_trackpad">Десен тракпад</string>
|
||||
|
||||
@@ -1245,6 +1245,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Páčka</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Přepnout mezi páčkou a trackpadem</string>
|
||||
<string name="deck_close">Zavřít ovládání druhé obrazovky</string>
|
||||
<string name="deck_open_steam_menu">Otevřít nabídku Steamu</string>
|
||||
<string name="deck_right_trackpad">Pravý trackpad</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Skift mellem stick og trackpad</string>
|
||||
<string name="deck_close">Luk kontrollerne til den anden skærm</string>
|
||||
<string name="deck_open_steam_menu">Åbn Steam-menuen</string>
|
||||
<string name="deck_right_trackpad">Højre trackpad</string>
|
||||
|
||||
@@ -1223,6 +1223,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Zwischen Stick und Trackpad wechseln</string>
|
||||
<string name="deck_close">Steuerung auf dem Zweitbildschirm schließen</string>
|
||||
<string name="deck_open_steam_menu">Steam-Menü öffnen</string>
|
||||
<string name="deck_right_trackpad">Rechtes Trackpad</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Χειριστήριο</string>
|
||||
<string name="deck_surface_stick">Μοχλός</string>
|
||||
<string name="deck_surface_trackpad">Πινακίδα αφής</string>
|
||||
<string name="deck_surface_switch">Εναλλαγή μεταξύ μοχλού και πινακίδας αφής</string>
|
||||
<string name="deck_close">Κλείσιμο χειριστηρίων δεύτερης οθόνης</string>
|
||||
<string name="deck_open_steam_menu">Άνοιγμα μενού Steam</string>
|
||||
<string name="deck_right_trackpad">Δεξιά πινακίδα αφής</string>
|
||||
|
||||
@@ -1238,6 +1238,7 @@
|
||||
<string name="deck_tab_gamepad">Mando</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Cambiar entre stick y trackpad</string>
|
||||
<string name="deck_close">Cerrar controles de la segunda pantalla</string>
|
||||
<string name="deck_open_steam_menu">Abrir menú de Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad derecho</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Peliohjain</string>
|
||||
<string name="deck_surface_stick">Tatti</string>
|
||||
<string name="deck_surface_trackpad">Kosketuslevy</string>
|
||||
<string name="deck_surface_switch">Vaihda tatin ja kosketuslevyn välillä</string>
|
||||
<string name="deck_close">Sulje toisen näytön ohjaimet</string>
|
||||
<string name="deck_open_steam_menu">Avaa Steam-valikko</string>
|
||||
<string name="deck_right_trackpad">Oikea kosketuslevy</string>
|
||||
|
||||
@@ -1238,6 +1238,7 @@
|
||||
<string name="deck_tab_gamepad">Manette</string>
|
||||
<string name="deck_surface_stick">Joystick</string>
|
||||
<string name="deck_surface_trackpad">Pavé tactile</string>
|
||||
<string name="deck_surface_switch">Basculer entre joystick et pavé tactile</string>
|
||||
<string name="deck_close">Fermer les commandes du deuxième écran</string>
|
||||
<string name="deck_open_steam_menu">Ouvrir le menu Steam</string>
|
||||
<string name="deck_right_trackpad">Pavé tactile droit</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">गेमपैड</string>
|
||||
<string name="deck_surface_stick">स्टिक</string>
|
||||
<string name="deck_surface_trackpad">ट्रैकपैड</string>
|
||||
<string name="deck_surface_switch">स्टिक और ट्रैकपैड के बीच बदलें</string>
|
||||
<string name="deck_close">दूसरी स्क्रीन के कंट्रोल बंद करें</string>
|
||||
<string name="deck_open_steam_menu">Steam मेनू खोलें</string>
|
||||
<string name="deck_right_trackpad">दायां ट्रैकपैड</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Kontroller</string>
|
||||
<string name="deck_surface_stick">Kar</string>
|
||||
<string name="deck_surface_trackpad">Érintőpad</string>
|
||||
<string name="deck_surface_switch">Váltás a kar és az érintőpad között</string>
|
||||
<string name="deck_close">Második képernyős vezérlők bezárása</string>
|
||||
<string name="deck_open_steam_menu">Steam-menü megnyitása</string>
|
||||
<string name="deck_right_trackpad">Jobb érintőpad</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stik</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Beralih antara stik dan trackpad</string>
|
||||
<string name="deck_close">Tutup kontrol layar kedua</string>
|
||||
<string name="deck_open_steam_menu">Buka menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad kanan</string>
|
||||
|
||||
@@ -1230,6 +1230,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Levetta</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alterna tra levetta e trackpad</string>
|
||||
<string name="deck_close">Chiudi i controlli sul secondo schermo</string>
|
||||
<string name="deck_open_steam_menu">Apri il menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad destro</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">ゲームパッド</string>
|
||||
<string name="deck_surface_stick">スティック</string>
|
||||
<string name="deck_surface_trackpad">トラックパッド</string>
|
||||
<string name="deck_surface_switch">スティックとトラックパッドを切り替え</string>
|
||||
<string name="deck_close">セカンドスクリーンコントロールを閉じる</string>
|
||||
<string name="deck_open_steam_menu">Steamメニューを開く</string>
|
||||
<string name="deck_right_trackpad">右トラックパッド</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">게임패드</string>
|
||||
<string name="deck_surface_stick">스틱</string>
|
||||
<string name="deck_surface_trackpad">터치패드</string>
|
||||
<string name="deck_surface_switch">스틱과 터치패드 간 전환</string>
|
||||
<string name="deck_close">보조 화면 컨트롤 닫기</string>
|
||||
<string name="deck_open_steam_menu">Steam 메뉴 열기</string>
|
||||
<string name="deck_right_trackpad">오른쪽 터치패드</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stik</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Tukar antara stik dan trackpad</string>
|
||||
<string name="deck_close">Tutup kawalan skrin kedua</string>
|
||||
<string name="deck_open_steam_menu">Buka menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad kanan</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Kontroller</string>
|
||||
<string name="deck_surface_stick">Stikke</string>
|
||||
<string name="deck_surface_trackpad">Styreflate</string>
|
||||
<string name="deck_surface_switch">Bytt mellom stikke og styreflate</string>
|
||||
<string name="deck_close">Lukk kontroller for andre skjerm</string>
|
||||
<string name="deck_open_steam_menu">Åpne Steam-menyen</string>
|
||||
<string name="deck_right_trackpad">Høyre styreflate</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Wisselen tussen stick en trackpad</string>
|
||||
<string name="deck_close">Bediening op tweede scherm sluiten</string>
|
||||
<string name="deck_open_steam_menu">Steam-menu openen</string>
|
||||
<string name="deck_right_trackpad">Rechtertrackpad</string>
|
||||
|
||||
@@ -1245,6 +1245,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Gałka</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Przełącz między gałką a trackpadem</string>
|
||||
<string name="deck_close">Zamknij sterowanie na drugim ekranie</string>
|
||||
<string name="deck_open_steam_menu">Otwórz menu Steam</string>
|
||||
<string name="deck_right_trackpad">Prawy trackpad</string>
|
||||
|
||||
@@ -1230,6 +1230,7 @@
|
||||
<string name="deck_tab_gamepad">Comando</string>
|
||||
<string name="deck_surface_stick">Joystick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alternar entre joystick e trackpad</string>
|
||||
<string name="deck_close">Fechar controlos do segundo ecrã</string>
|
||||
<string name="deck_open_steam_menu">Abrir menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad direito</string>
|
||||
|
||||
@@ -1230,6 +1230,7 @@
|
||||
<string name="deck_tab_gamepad">Controle</string>
|
||||
<string name="deck_surface_stick">Analógico</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alternar entre analógico e trackpad</string>
|
||||
<string name="deck_close">Fechar controles da segunda tela</string>
|
||||
<string name="deck_open_steam_menu">Abrir o menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad direito</string>
|
||||
|
||||
@@ -1238,6 +1238,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Manșă</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Comută între manșă și trackpad</string>
|
||||
<string name="deck_close">Închide comenzile celui de-al doilea ecran</string>
|
||||
<string name="deck_open_steam_menu">Deschide meniul Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad-ul drept</string>
|
||||
|
||||
@@ -1282,6 +1282,7 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стик</string>
|
||||
<string name="deck_surface_trackpad">Тачпад</string>
|
||||
<string name="deck_surface_switch">Переключение между стиком и тачпадом</string>
|
||||
<string name="deck_close">Закрыть управление вторым экраном</string>
|
||||
<string name="deck_open_steam_menu">Открыть меню Steam</string>
|
||||
<string name="deck_right_trackpad">Правый тачпад</string>
|
||||
|
||||
@@ -1231,6 +1231,7 @@
|
||||
<string name="deck_tab_gamepad">Handkontroll</string>
|
||||
<string name="deck_surface_stick">Styrspak</string>
|
||||
<string name="deck_surface_trackpad">Styrplatta</string>
|
||||
<string name="deck_surface_switch">Växla mellan styrspak och styrplatta</string>
|
||||
<string name="deck_close">Stäng kontrollerna på andra skärmen</string>
|
||||
<string name="deck_open_steam_menu">Öppna Steam-menyn</string>
|
||||
<string name="deck_right_trackpad">Höger styrplatta</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">เกมแพด</string>
|
||||
<string name="deck_surface_stick">สติ๊ก</string>
|
||||
<string name="deck_surface_trackpad">แทร็กแพด</string>
|
||||
<string name="deck_surface_switch">สลับระหว่างสติ๊กและแทร็กแพด</string>
|
||||
<string name="deck_close">ปิดการควบคุมหน้าจอที่สอง</string>
|
||||
<string name="deck_open_steam_menu">เปิดเมนู Steam</string>
|
||||
<string name="deck_right_trackpad">แทร็กแพดขวา</string>
|
||||
|
||||
@@ -1223,6 +1223,7 @@
|
||||
<string name="deck_tab_gamepad">Oyun kolu</string>
|
||||
<string name="deck_surface_stick">Çubuk</string>
|
||||
<string name="deck_surface_trackpad">Dokunmatik yüzey</string>
|
||||
<string name="deck_surface_switch">Çubuk ile dokunmatik yüzey arasında geçiş yap</string>
|
||||
<string name="deck_close">İkinci ekran kontrollerini kapat</string>
|
||||
<string name="deck_open_steam_menu">Steam menüsünü aç</string>
|
||||
<string name="deck_right_trackpad">Sağ dokunmatik yüzey</string>
|
||||
|
||||
@@ -1245,6 +1245,7 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стік</string>
|
||||
<string name="deck_surface_trackpad">Трекпад</string>
|
||||
<string name="deck_surface_switch">Перемкнути між стіком і трекпадом</string>
|
||||
<string name="deck_close">Закрити керування на другому екрані</string>
|
||||
<string name="deck_open_steam_menu">Відкрити меню Steam</string>
|
||||
<string name="deck_right_trackpad">Правий трекпад</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">Tay cầm</string>
|
||||
<string name="deck_surface_stick">Cần</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Chuyển giữa cần và trackpad</string>
|
||||
<string name="deck_close">Đóng điều khiển màn hình phụ</string>
|
||||
<string name="deck_open_steam_menu">Mở menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad phải</string>
|
||||
|
||||
@@ -1308,6 +1308,7 @@
|
||||
<string name="deck_tab_gamepad">手柄</string>
|
||||
<string name="deck_surface_stick">摇杆</string>
|
||||
<string name="deck_surface_trackpad">触控板</string>
|
||||
<string name="deck_surface_switch">在摇杆与触控板之间切换</string>
|
||||
<string name="deck_close">关闭副屏控制</string>
|
||||
<string name="deck_open_steam_menu">打开 Steam 菜单</string>
|
||||
<string name="deck_right_trackpad">右触控板</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">控制器</string>
|
||||
<string name="deck_surface_stick">搖桿</string>
|
||||
<string name="deck_surface_trackpad">觸控板</string>
|
||||
<string name="deck_surface_switch">在搖桿和觸控板之間切換</string>
|
||||
<string name="deck_close">關閉第二螢幕控制</string>
|
||||
<string name="deck_open_steam_menu">開啟 Steam 選單</string>
|
||||
<string name="deck_right_trackpad">右側觸控板</string>
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
<string name="deck_tab_gamepad">手把</string>
|
||||
<string name="deck_surface_stick">搖桿</string>
|
||||
<string name="deck_surface_trackpad">觸控板</string>
|
||||
<string name="deck_surface_switch">在搖桿與觸控板之間切換</string>
|
||||
<string name="deck_close">關閉第二螢幕控制</string>
|
||||
<string name="deck_open_steam_menu">開啟 Steam 選單</string>
|
||||
<string name="deck_right_trackpad">右側觸控板</string>
|
||||
|
||||
@@ -1382,6 +1382,7 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Switch between stick and trackpad</string>
|
||||
<string name="deck_close">Close second-screen controls</string>
|
||||
<string name="deck_open_steam_menu">Open Steam menu</string>
|
||||
<string name="deck_right_trackpad">Right trackpad</string>
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Looper
|
||||
import com.droiddeck.launcher.session.QuestExperimental
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.gpu.TpUbwcHint
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -35,31 +34,6 @@ class DriverMenusAutoTest {
|
||||
ShadowBuild.setDevice("eureka")
|
||||
}
|
||||
|
||||
@Test fun pickingTheAutoDisplayHandsBothHalvesToAuto() {
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MIX)
|
||||
SessionPrefs.setAndroidDriver(activity, "chosen-display")
|
||||
SessionPrefs.setLinuxDriver(activity, "chosen-linux")
|
||||
val menus = DriverMenus(activity, Handler(Looper.getMainLooper()))
|
||||
menus.selectDriver(TurnipDriver.AUTO, linux = false)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(activity))
|
||||
assertEquals(QuestExperimental.DRIVER, SessionPrefs.androidDriver(activity))
|
||||
assertEquals("", SessionPrefs.linuxDriver(activity))
|
||||
}
|
||||
|
||||
@Test fun pickingTheAutoDisplayWithNoPairForTheGpuStillPutsTheDisplayOnAuto() {
|
||||
ShadowBuild.setModel("Generic phone")
|
||||
ShadowBuild.setDevice("generic")
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MIX)
|
||||
SessionPrefs.setAndroidDriver(activity, "chosen-display")
|
||||
SessionPrefs.setLinuxDriver(activity, "chosen-linux")
|
||||
DriverMenus(activity, Handler(Looper.getMainLooper())).selectDriver(TurnipDriver.AUTO, linux = false)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(activity))
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(activity))
|
||||
assertEquals("chosen-linux", SessionPrefs.linuxDriver(activity))
|
||||
}
|
||||
|
||||
@Test fun selectingAutoUsesTheQuestPairWithoutStartingADownload() {
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MANUAL)
|
||||
|
||||
@@ -45,30 +45,6 @@ class LibraryShortcutsLastPlayedTest {
|
||||
assertEquals(mapOf(3388195926L to 1792931476L, 1234L to 1700000000L), Library.shortcutsLastPlayed(root))
|
||||
}
|
||||
|
||||
@Test fun aLaunchedAddedGameRemembersItsPlayTime() {
|
||||
val context = org.robolectric.RuntimeEnvironment.getApplication()
|
||||
val added = Library.SteamGame(-1, "World of Warcraft", null, Library.ADDED, gameId = 0xC0FFEE0002000000uL.toLong())
|
||||
val steam = Library.SteamGame(570, "Dota 2", null, "/steam")
|
||||
assertEquals(0L, Library.recordedPlay(context, added.gameId))
|
||||
val before = System.currentTimeMillis() / 1000
|
||||
Library.recordPlayed(context, added)
|
||||
Library.recordPlayed(context, steam)
|
||||
assert(Library.recordedPlay(context, added.gameId) >= before)
|
||||
assertEquals(0L, Library.recordedPlay(context, steam.gameId))
|
||||
}
|
||||
|
||||
@Test fun anAddedGameBuiltAgainKeepsItsRecordedPlayTime() {
|
||||
val context = org.robolectric.RuntimeEnvironment.getApplication()
|
||||
val dir = tmp.newFolder("Celeste")
|
||||
val exe = File(dir, "Celeste.exe").apply { writeText("x") }
|
||||
val g = AddedGames.Game(dir, "Celeste", exe, "/g/Celeste.exe", "/g", 0x82000003L, 0x82000003L, emptyList())
|
||||
val shown = Library.addedGame(context, g, emptyList())
|
||||
assertEquals(0L, shown.lastPlayed)
|
||||
Library.recordPlayed(context, shown)
|
||||
assertEquals(Library.recordedPlay(context, g.gameId), Library.addedGame(context, g, emptyList()).lastPlayed)
|
||||
assertEquals(1900000000L, Library.addedGame(context, g, emptyList(), lastPlayed = 1900000000L).lastPlayed)
|
||||
}
|
||||
|
||||
@Test fun missingOrBrokenShortcutsGiveNothing() {
|
||||
val root = steamRoot()
|
||||
assertEquals(emptyMap<Long, Long>(), Library.shortcutsLastPlayed(root))
|
||||
|
||||
@@ -101,33 +101,6 @@ class DriverBundleTest {
|
||||
assertTrue(DriverBundle.all(context).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun aDisplayPickedAwayFromABundleTakesItsRuntimeHalfWithIt() {
|
||||
val b = DriverBundle.install(context, bundle())
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, TurnipDriver.AUTO)
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
assertNull(DriverBundle.active(context))
|
||||
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, "turnip25.1.0")
|
||||
assertEquals("turnip25.1.0", SessionPrefs.androidDriver(context))
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
}
|
||||
|
||||
@Test fun aDisplayPickOutsideABundleKeepsTheChosenRuntime() {
|
||||
SessionPrefs.setLinuxDriver(context, "my-linux-driver")
|
||||
SessionPrefs.setAndroidDriver(context, "turnip25.1.0")
|
||||
DriverBundle.selectDisplay(context, TurnipDriver.AUTO)
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
|
||||
assertEquals("my-linux-driver", SessionPrefs.linuxDriver(context))
|
||||
|
||||
val b = DriverBundle.install(context, bundle())
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, b.androidId)
|
||||
assertEquals(b.linuxId, SessionPrefs.linuxDriver(context))
|
||||
}
|
||||
|
||||
@Test fun aRefusedHalfLeavesNothingInstalled() {
|
||||
try {
|
||||
DriverBundle.install(context, bundle(linuxLibc = "libc.so"))
|
||||
|
||||
@@ -48,17 +48,6 @@ class BottomScreenGuestsTest {
|
||||
assertEquals(null, BottomScreenGuests.holder(null, null, homes))
|
||||
}
|
||||
|
||||
@Test fun anAppThatArrivedBeforeDroidDeckCoveredTheScreenAgainNoLongerHoldsIt() {
|
||||
assertFalse(BottomScreenGuests.current(arrivedAt = 1_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 3_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 2_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 1_000, ownShownAt = null))
|
||||
fun holder(arrivedAt: Long, ownShownAt: Long) = BottomScreenGuests.holder(null,
|
||||
"com.android.chrome".takeIf { BottomScreenGuests.current(arrivedAt, ownShownAt) }, homes)
|
||||
assertEquals(null, holder(arrivedAt = 1_000, ownShownAt = 2_000))
|
||||
assertEquals("com.android.chrome", holder(arrivedAt = 3_000, ownShownAt = 2_000))
|
||||
}
|
||||
|
||||
@Test fun keyboardsAndOverlaysNeverTakeTheScreen() {
|
||||
assertTrue(BottomScreenGuests.ignorable(AccessibilityWindowInfo.TYPE_INPUT_METHOD))
|
||||
assertTrue(BottomScreenGuests.ignorable(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY))
|
||||
|
||||
@@ -141,26 +141,6 @@ class DirectTouchHoldTest {
|
||||
assertEquals(listOf("true at 40.0,30.0", "false at 40.0,30.0"), buttons)
|
||||
}
|
||||
|
||||
@Test fun cancelTouchEndsALiveTouchAndKeepsAHeldButton() {
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
touch(MotionEvent.ACTION_MOVE, 60f)
|
||||
hold.cancelTouch()
|
||||
assertEquals(listOf("down 0 at 40.0,30.0", "move 0 at 60.0,30.0", "cancel"), touches)
|
||||
assertTrue(buttons.isEmpty())
|
||||
|
||||
touches.clear()
|
||||
hold.configure(TouchGestureConfig(longPressDelay = 500,
|
||||
directHoldBehavior = TouchGestureConfig.MOUSE_BEHAVIOR_HOLD))
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
advance(500)
|
||||
hold.cancelTouch()
|
||||
advance(1000)
|
||||
assertEquals(listOf("true at 40.0,30.0"), buttons)
|
||||
assertTrue(touches.isEmpty())
|
||||
touch(MotionEvent.ACTION_UP)
|
||||
assertEquals(listOf("true at 40.0,30.0", "false at 40.0,30.0"), buttons)
|
||||
}
|
||||
|
||||
@Test fun cancelForwardsACancelWhenTheTouchIsLive() {
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
touch(MotionEvent.ACTION_MOVE, 60f)
|
||||
|
||||
@@ -205,28 +205,6 @@ class OnScreenControlsTest {
|
||||
assertFalse(view.isQuickHidden)
|
||||
}
|
||||
|
||||
@Test fun hidingTheGamepadKeepsAlwaysShownKeysDrawnAndTouchable() {
|
||||
val key = InputSurfaceElement(InputControl.A, .45f, .1f, .1f, .1f, "imported:0", name = "Find", alwaysShown = true)
|
||||
view.updateLayout(SavedInputLayout("test", "Test", listOf(left, button, key), aspect = ASPECT))
|
||||
measure(view)
|
||||
assertTrue(view.hasPinned)
|
||||
view.setPinnedOnly(true)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
assertFalse(view.takesTouchAt(800f, 250f))
|
||||
assertFalse(view.takesTouchAt(200f, 300f))
|
||||
view.setPinnedOnly(false)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(800f, 250f))
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
view.setQuickHidden(true)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
assertFalse(view.takesTouchAt(800f, 250f))
|
||||
view.updateLayout(SavedInputLayout("test", "Test", listOf(left, button), aspect = ASPECT))
|
||||
assertFalse(view.hasPinned)
|
||||
}
|
||||
|
||||
private companion object { const val ASPECT = 1000f / 600f }
|
||||
|
||||
private fun touch(action: Int, vararg pointers: Pair<Int, Pair<Float, Float>>, flags: Int = 0): Boolean {
|
||||
|
||||
@@ -19,7 +19,7 @@ class OnScreenLayoutTest {
|
||||
private val density get() = context.resources.displayMetrics.density
|
||||
|
||||
@Before fun reset() {
|
||||
for (name in listOf("input-profiles", "input-surfaces", "controller", "session")) context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
for (name in listOf("input-profiles", "input-surfaces", "controller")) context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
private fun gamepad() = InputLibraries.read(context).layouts.first { it.id == InputLibraries.OVERLAY }
|
||||
@@ -188,105 +188,4 @@ class OnScreenLayoutTest {
|
||||
val selected = old.selectProfile(false, "migrated.screenkeys.0").selectProfile(true, "migrated.screenkeys.0")
|
||||
assertEquals(selected, LegacyScreenKeyMigration.merge(selected, shown))
|
||||
}
|
||||
|
||||
@Test fun alwaysShownKeysStayInButtonsOnlyModeAndLeaveTheUnfoldedStrip() {
|
||||
val key = InputSurfaceElement(InputControl.A, .45f, .1f, .1f, .1f, "imported:0", name = "Find", alwaysShown = true)
|
||||
val plain = InputSurfaceElement(InputControl.A, .45f, .4f, .1f, .1f, "imported:1", name = "Jump")
|
||||
val layout = gamepad().let { it.copy(elements = it.elements + key + plain) }
|
||||
val buttons = OnScreenLayout.elements(context, layout, 1000, 500, Rect(), buttonsOnly = true)
|
||||
assertTrue(buttons.any { it.id == key.id })
|
||||
assertFalse(buttons.any { it.id == plain.id })
|
||||
assertEquals(OnScreenLayout.elements(context, gamepad(), 1000, 500, Rect(), buttonsOnly = true), buttons.filterNot { it.id == key.id })
|
||||
val strip = OnScreenLayout.elements(context, layout, 2208, 800, Rect(), dock = OnScreenLayout.Dock(false, grips = false))
|
||||
assertFalse(strip.any { it.id == key.id })
|
||||
assertTrue(strip.any { it.id == plain.id })
|
||||
assertTrue(OnScreenLayout.elements(context, layout, 1000, 500, Rect()).map { it.id }.containsAll(listOf(key.id, plain.id)))
|
||||
}
|
||||
|
||||
@Test fun migratedScreenKeysAreAlwaysShownButOtherElementsAreNot() {
|
||||
prefs.edit().clear().commit()
|
||||
ControllerPrefs.prefs(context).edit().putString("screenKeys", legacyKeys).commit()
|
||||
val migrated = InputLibraries.read(context)
|
||||
assertTrue(importedOn(migrated).all { it.alwaysShown })
|
||||
assertTrue(migrated.layouts.first { it.id == "migrated.screenkeys.0.layout" }.elements.all { it.alwaysShown })
|
||||
assertTrue(migrated.layouts.flatMap { it.elements }.filterNot { it.id.startsWith("imported:") }.none { it.alwaysShown })
|
||||
assertFalse(LegacyScreenKeyMigration.convert(context, legacyKeys, 1000, 500, 2f).any { it.first.alwaysShown })
|
||||
}
|
||||
|
||||
@Test fun keysMergedByAnEarlierVersionBecomeAlwaysShownOnUpgrade() {
|
||||
val fresh = InputLibraries.read(context)
|
||||
val backup = JSONObject().put("screenKeys", legacyKeys).toString()
|
||||
val unpinned = LegacyScreenKeyMigration.merge(LegacyScreenKeyMigration.migrate(context, fresh, backup), backup).let { library ->
|
||||
library.copy(layouts = library.layouts.map { l -> l.copy(elements = l.elements.map { it.copy(alwaysShown = false) }) })
|
||||
}
|
||||
val other = SavedInputLayout("user.keys", "Mine", listOf(InputSurfaceElement(InputControl.A, .1f, .1f, .1f, .1f, "imported:0", name = "Other")))
|
||||
val stored = unpinned.withLayout(other)
|
||||
prefs.edit().putString("library", JSONObject(InputLibraries.encode(stored)).put("version", 12).toString())
|
||||
.putString(LegacyScreenKeyMigration.BACKUP, backup).putBoolean(LegacyScreenKeyMigration.MERGED, true).commit()
|
||||
val upgraded = InputLibraries.read(context)
|
||||
assertEquals(1, importedOn(upgraded).size)
|
||||
assertTrue(importedOn(upgraded).single().alwaysShown)
|
||||
assertTrue(upgraded.layouts.first { it.id == "migrated.screenkeys.0.layout" }.elements.all { it.alwaysShown })
|
||||
assertFalse(upgraded.layouts.first { it.id == "user.keys" }.elements.single().alwaysShown)
|
||||
assertEquals(stored.layouts.map { l -> l.elements.map { it.copy(alwaysShown = false) } }, upgraded.layouts.filter { l -> stored.layouts.any { it.id == l.id } }
|
||||
.map { l -> l.elements.map { it.copy(alwaysShown = false) } })
|
||||
}
|
||||
|
||||
@Test fun aDeckStripPutsTheDeckGamepadSurfacesInTheStickSlotsAndOtherPadsKeepSticks() {
|
||||
val stock = InputStockLayouts.layouts().first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
val deck = OnScreenLayout.Dock.deck(false, stock)
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LS, InputControl.RIGHT_PAD), deck)
|
||||
assertEquals(deck, OnScreenLayout.Dock.deck(false, null))
|
||||
val (w, h) = 2208 to 800
|
||||
val boxes = GamepadGeometry.layout(w.toFloat(), h.toFloat(), density, false)
|
||||
val placed = OnScreenLayout.elements(context, gamepad(), w, h, Rect(), dock = deck)
|
||||
val pad = placed.single { it.control == InputControl.RIGHT_PAD }
|
||||
assertEquals(InputElementKind.TRACKPAD, pad.kind)
|
||||
assertEquals(boxes.getValue(GamepadGeometry.Slot.RIGHT_MAIN).left, pad.x * w, 1f)
|
||||
assertNull(placed.firstOrNull { it.control == InputControl.RS })
|
||||
assertTrue(placed.any { it.control == InputControl.LS })
|
||||
val plain = OnScreenLayout.elements(context, gamepad(), w, h, Rect(), dock = OnScreenLayout.Dock(false, false))
|
||||
assertTrue(plain.any { it.control == InputControl.RS })
|
||||
assertNull(plain.firstOrNull { it.kind == InputElementKind.TRACKPAD })
|
||||
val swapped = stock.copy(elements = stock.elements.map {
|
||||
when (it.control) {
|
||||
InputControl.LS -> it.copy(control = InputControl.LEFT_PAD, id = "legacy:left-pad", kind = InputElementKind.TRACKPAD)
|
||||
InputControl.RIGHT_PAD -> it.copy(control = InputControl.RS, id = "legacy:rs", kind = InputElementKind.STICK)
|
||||
else -> it
|
||||
}
|
||||
})
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LEFT_PAD, InputControl.RS), OnScreenLayout.Dock.deck(false, swapped))
|
||||
}
|
||||
|
||||
private fun deckGamepad(library: InputLibrary) = library.layouts.first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
|
||||
private fun upgradeWithSurfaces(left: String?, right: String?, change: (InputLibrary) -> InputLibrary = { it }): InputLibrary {
|
||||
val stored = change(InputLibraries.read(context))
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().putString("gamepadLeft", left).putString("gamepadRight", right).commit()
|
||||
prefs.edit().putString("library", JSONObject(InputLibraries.encode(stored)).put("version", 13).toString()).commit()
|
||||
return InputLibraries.read(context)
|
||||
}
|
||||
|
||||
@Test fun theSavedDeckGamepadStickOrTrackpadChoiceMovesIntoTheUntouchedDeckGamepadOnce() {
|
||||
val upgraded = upgradeWithSurfaces("trackpad", "stick")
|
||||
val elements = deckGamepad(upgraded).elements
|
||||
assertEquals(InputStockLayouts.gamepad(leftPad = true, rightStick = true), elements)
|
||||
assertEquals(InputElementKind.TRACKPAD, elements.single { it.control == InputControl.LEFT_PAD }.kind)
|
||||
assertEquals(InputElementKind.STICK, elements.single { it.control == InputControl.RS }.kind)
|
||||
assertNull(elements.firstOrNull { it.control == InputControl.LS || it.control == InputControl.RIGHT_PAD })
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LEFT_PAD, InputControl.RS), OnScreenLayout.Dock.deck(false, deckGamepad(upgraded)))
|
||||
assertEquals(upgraded, InputLibraries.read(context))
|
||||
}
|
||||
|
||||
@Test fun theDefaultDeckGamepadChoiceOrAnEditedDeckGamepadIsLeftAlone() {
|
||||
val stock = InputStockLayouts.layouts().first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
assertEquals(stock.elements, deckGamepad(upgradeWithSurfaces(null, null)).elements)
|
||||
assertEquals(stock.elements, deckGamepad(upgradeWithSurfaces("stick", "trackpad")).elements)
|
||||
val moved = stock.elements.map { if (it.control == InputControl.A) it.copy(x = .7f) else it }
|
||||
val edited = upgradeWithSurfaces("trackpad", "stick") { it.withLayout(deckGamepad(it).copy(elements = moved)) }
|
||||
assertEquals(moved, deckGamepad(edited).elements)
|
||||
val others = InputLibraries.read(context).layouts.filter { it.id != DeckPanel.GAMEPAD.layout }
|
||||
assertEquals(InputStockLayouts.layouts().filter { it.id != DeckPanel.GAMEPAD.layout }.map { it.elements },
|
||||
others.filter { l -> InputStockLayouts.layouts().any { it.id == l.id } }.map { it.elements })
|
||||
}
|
||||
}
|
||||
@@ -64,13 +64,4 @@ class SecondScreenModeTest {
|
||||
assertEquals(SecondScreenMode.TERMINAL, SecondScreenMode.opening("terminal", deckPad, dashboard, desktopSession = true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun theEditorOpensOnTheChosenBottomScreenUnlessAnotherAppHoldsIt() {
|
||||
val free = { _: Int -> false }
|
||||
assertEquals(listOf(2, 3), SecondScreenDisplays.editorCandidates(2, listOf(2, 3), free))
|
||||
assertEquals(listOf(3, 2), SecondScreenDisplays.editorCandidates(3, listOf(2, 3), free))
|
||||
assertEquals(listOf(-1, 2), SecondScreenDisplays.editorCandidates(-1, listOf(2), free))
|
||||
assertEquals(listOf(3), SecondScreenDisplays.editorCandidates(2, listOf(2, 3)) { it == 2 })
|
||||
assertEquals(emptyList<Int>(), SecondScreenDisplays.editorCandidates(2, listOf(2)) { it == 2 })
|
||||
}
|
||||
}
|
||||
@@ -95,14 +95,6 @@ class GameStorageTest {
|
||||
assertFalse(GameStorage.attached(File("/mnt/media_rw/B2BD-BCFC/STEAM GAMES")) { null })
|
||||
}
|
||||
|
||||
@Test fun aSavedFolderThatIsThereIsAttachedWhateverItsVolumeReports() {
|
||||
val folder = tmp.newFolder("card", "STEAM GAMES")
|
||||
listOf(Environment.MEDIA_UNKNOWN, Environment.MEDIA_UNMOUNTED, Environment.MEDIA_REMOVED, null).forEach {
|
||||
assertTrue(GameStorage.attached(folder, it, externalStoragePath = true))
|
||||
}
|
||||
assertTrue(GameStorage.attached(folder) { Environment.MEDIA_UNKNOWN })
|
||||
}
|
||||
|
||||
@Test fun onlyASavedDriveThatIsMissingCountsAsDisconnected() {
|
||||
SessionPrefs.setGameStorage(app, "", "")
|
||||
assertFalse(GameStorage.disconnected(app))
|
||||
|
||||
@@ -20,14 +20,6 @@ class SortCatalogItemsTest {
|
||||
assertEquals(listOf("Zelda", "Beta", "alpha"), sortCatalogItems(items, "titleDesc").map { it.title })
|
||||
}
|
||||
|
||||
@Test fun anEntryTimeFallsBackWhenTheFirstSourceHasNone() {
|
||||
assertEquals(30L, entryTime(30L, 20L))
|
||||
assertEquals(20L, entryTime(0L, 20L))
|
||||
assertEquals(20L, entryTime(null, 20L))
|
||||
assertEquals(0L, entryTime(0L, null))
|
||||
assertEquals(0L, entryTime(null, 0L))
|
||||
}
|
||||
|
||||
@Test fun playedAndInstalledComeFirstWithTitlesBreakingTies() {
|
||||
val items = listOf(item("1", "Zelda"), item("2", "alpha"), item("3", "Beta"))
|
||||
val played = mapOf("gog:3" to 900L, "gog:1" to 900L)
|
||||
|
||||
@@ -383,10 +383,9 @@ class InputSettingsTest {
|
||||
|
||||
@Test fun elementPropertiesEditOpacityLabelsAndOfferImages() {
|
||||
val element = mutableStateOf(InputSurfaceElement(InputControl.A, .1f, .1f, .2f, .2f, "custom", name = "Custom button"))
|
||||
val action = mutableStateOf<InputAction?>(null)
|
||||
compose.setContent { DroidDeckTheme {
|
||||
androidx.compose.foundation.layout.Column(androidx.compose.ui.Modifier.verticalScroll(androidx.compose.foundation.rememberScrollState())) {
|
||||
InputElementProperties(element.value, action.value) { element.value = it }
|
||||
InputElementProperties(element.value) { element.value = it }
|
||||
}
|
||||
} }
|
||||
compose.onNodeWithTag("setting-element-opacity").performScrollTo().performSemanticsAction(androidx.compose.ui.semantics.SemanticsActions.SetProgress) { it(60f) }
|
||||
@@ -400,17 +399,6 @@ class InputSettingsTest {
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(InputControl.LEFT_PAD, .1f, .1f, .4f, .4f, "pad", InputElementKind.TRACKPAD, "Pad") }
|
||||
compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
compose.onNodeWithTag("setting-element-opacity").assertExists()
|
||||
for (kind in listOf(InputElementKind.DPAD, InputElementKind.STICK)) {
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(kind.control, .1f, .1f, .4f, .4f, "imported:0", kind, "WASD"); action.value = InputAction.Directions(17, 32, 31, 30) }
|
||||
compose.onNodeWithTag("setting-element-label").performScrollTo().performClick()
|
||||
compose.runOnIdle { assertFalse(element.value.showLabel); action.value = InputAction.Native() }
|
||||
compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
compose.runOnIdle { action.value = InputAction.Directions(0, 0, 0, 0, listOf(16, 18, 44, 46)) }
|
||||
if (kind == InputElementKind.STICK) compose.onNodeWithTag("setting-element-label").assertExists()
|
||||
else compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
}
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(InputControl.LS, .1f, .1f, .4f, .4f, "legacy:left-stick", InputElementKind.STICK, "Move") }
|
||||
compose.onNodeWithTag("setting-element-label").assertExists()
|
||||
}
|
||||
|
||||
@Test fun editorScalesEveryControlAndRestoresStockPositions() {
|
||||
@@ -433,23 +421,6 @@ class InputSettingsTest {
|
||||
compose.runOnIdle { assertEquals(layout.elements, InputLibraries.read(context).layouts.first { it.id == layout.id }.elements) }
|
||||
}
|
||||
|
||||
@Test fun editorSnapChoiceIsRemembered() {
|
||||
com.droiddeck.launcher.input.ControllerPrefs.setSnap(context, true)
|
||||
val library = InputLibraries.read(context)
|
||||
val layout = library.layout("game", InputRole.SECONDARY)!!
|
||||
val round = mutableStateOf(0)
|
||||
compose.setContent { DroidDeckTheme { androidx.compose.runtime.key(round.value) { InputSurfaceDialog("game", {}, {}, role = InputRole.PRIMARY,
|
||||
layoutId = layout.id, bindingsId = library.profile("game").binding(InputRole.SECONDARY)) } } }
|
||||
val snap = isToggleable() and hasAnySibling(hasText(context.getString(R.string.ctrl_editor_snap)))
|
||||
compose.onNodeWithTag("input-editor-tools").performClick()
|
||||
compose.onNode(snap).performScrollTo().assertIsOn().performClick()
|
||||
compose.runOnIdle { assertFalse(com.droiddeck.launcher.input.ControllerPrefs.snap(context)) }
|
||||
compose.runOnIdle { round.value = 1 }
|
||||
compose.onNodeWithTag("input-editor-tools").performClick()
|
||||
compose.onNode(snap).performScrollTo().assertIsOff()
|
||||
com.droiddeck.launcher.input.ControllerPrefs.setSnap(context, true)
|
||||
}
|
||||
|
||||
@Test fun quickButtonTogglesHideBuiltInButtonsWithoutDeletingThem() {
|
||||
compose.setContent { DroidDeckTheme { InputSettingsPage({}) } }
|
||||
compose.onNodeWithTag("input-profile-game").performScrollTo().performClick()
|
||||
|
||||
@@ -58,16 +58,6 @@ class InputSurfaceViewTest {
|
||||
} finally { root.removeAllViews(); bridge.stop(); controller.pause().stop().destroy() }
|
||||
}
|
||||
|
||||
@Test fun keyedDirectionsAreLabelledWithTheirKeysAndOtherActionsAreNot() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val wasd = InputAction.Directions(17, 32, 31, 30, listOf(18, 0, 0, 16))
|
||||
assertEquals(listOf((0f to -1f) to "W", (1f to 0f) to "D", (0f to 1f) to "S", (-1f to 0f) to "A"),
|
||||
InputSurfaceView.directionLabels(context, wasd, cardinalOnly = true))
|
||||
assertEquals(listOf("W", "D", "S", "A", "E", "Q"), InputSurfaceView.directionLabels(context, wasd, cardinalOnly = false).map { it.second })
|
||||
assertEquals(listOf("W", "A"), InputSurfaceView.directionLabels(context, InputAction.Directions(17, 0, 0, 30), cardinalOnly = false).map { it.second })
|
||||
assertTrue(InputSurfaceView.directionLabels(context, null, cardinalOnly = false).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun takesTouchAtMatchesWhatATouchDoes() {
|
||||
val directory = Files.createTempDirectory("deck-panel").toFile()
|
||||
try {
|
||||
|
||||
@@ -99,7 +99,7 @@ class SecondScreenBarTest {
|
||||
} finally { panel.closeControls() }
|
||||
}
|
||||
|
||||
@Test fun deckPanelHasTheBarWithSwapAndTabsAndProfileControlsKeepTheBar() {
|
||||
@Test fun deckPanelHasTheBarWithSwapAndTabsAndProfileControlsStayFullScreen() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
SessionState.deckPad = true
|
||||
var swaps = 0
|
||||
@@ -116,14 +116,8 @@ class SecondScreenBarTest {
|
||||
assertEquals(DeckPanel.entries.map { context.getString(it.label) }, bar.tabs!!.labels)
|
||||
assertEquals(DeckPanel.BOTH.ordinal, bar.tabs!!.selected)
|
||||
panel.showMode(SecondScreenMode.CUSTOM_CONTROLS)
|
||||
val profile = panel.barModel(SecondScreenMode.CUSTOM_CONTROLS)!!
|
||||
assertEquals(context.getString(R.string.input_profile_controls_mode), profile.title)
|
||||
assertEquals(listOf(context.getString(R.string.swap_screens)), profile.extras.map { it.description })
|
||||
assertNotNull(profile.onClose); assertNotNull(profile.onMenu)
|
||||
profile.extras.single().onClick()
|
||||
assertEquals(2, swaps)
|
||||
assertTrue(rootChildren(panel)[0] is ComposeView)
|
||||
assertTrue(rootChildren(panel)[1] is InputSurfaceView)
|
||||
assertNull(panel.barModel(SecondScreenMode.CUSTOM_CONTROLS))
|
||||
assertTrue(rootChildren(panel)[0] is InputSurfaceView)
|
||||
} finally { panel.closeControls() }
|
||||
val noSwap = presentation(swap = null)
|
||||
try {
|
||||
@@ -227,47 +221,6 @@ class SecondScreenBarTest {
|
||||
compose.onNodeWithTag("bottom-bar-close").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun swappedTopScreenProfileControlsHaveTheBarWithSwap() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
ReflectionHelpers.getField<MutableState<SecondScreenMode>>(activity, "secondScreenMode\$delegate").value = SecondScreenMode.CUSTOM_CONTROLS
|
||||
val method = SessionActivity::class.java.getDeclaredMethod("SwappedTopPanel", androidx.compose.runtime.Composer::class.java, Int::class.javaPrimitiveType)
|
||||
method.isAccessible = true
|
||||
compose.setContent { DroidDeckTheme { method.invoke(activity, currentComposer, 0) } }
|
||||
compose.onNodeWithText(context.getString(R.string.input_profile_controls_mode)).assertIsDisplayed()
|
||||
compose.onNodeWithContentDescription(context.getString(R.string.swap_screens)).assertIsDisplayed()
|
||||
compose.onNodeWithTag("bottom-bar-close").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun theBottomPanelWaitsUnderAnOpenInputEditorAndReturnsWhenItCloses() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val id = org.robolectric.shadows.ShadowDisplayManager.addDisplay("w1280dp-h960dp")
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
val displays = activity.getSystemService(DisplayManager::class.java)
|
||||
org.robolectric.Shadows.shadowOf(displays.getDisplay(id)).setFlags(Display.FLAG_PRESENTATION)
|
||||
ReflectionHelpers.setField(activity, "displayManager", displays)
|
||||
ReflectionHelpers.setField(activity, "started", true)
|
||||
ReflectionHelpers.getField<MutableState<Int>>(activity, "selectedSecondScreenDisplay\$delegate").value = id
|
||||
val mode = ReflectionHelpers.getField<MutableState<SecondScreenMode>>(activity, "secondScreenMode\$delegate")
|
||||
mode.value = SecondScreenMode.KEYBOARD_TRACKPAD
|
||||
fun panel() = ReflectionHelpers.getField<SecondScreenPresentation?>(activity, "secondScreenPresentation")
|
||||
fun show() = ReflectionHelpers.callInstanceMethod<Unit>(activity, "showSecondScreen",
|
||||
ReflectionHelpers.ClassParameter.from(SecondScreenMode::class.java, SecondScreenMode.KEYBOARD_TRACKPAD))
|
||||
activity.inputWindowShown(Display.DEFAULT_DISPLAY)
|
||||
activity.inputWindowShown(id)
|
||||
activity.inputWindowShown(id)
|
||||
show()
|
||||
assertNull(panel())
|
||||
assertEquals(SecondScreenMode.KEYBOARD_TRACKPAD, mode.value)
|
||||
activity.inputWindowClosed(id)
|
||||
assertNull(panel())
|
||||
activity.inputWindowClosed(id)
|
||||
try {
|
||||
assertEquals(id, panel()!!.display.displayId)
|
||||
assertTrue(panel()!!.isShowing)
|
||||
} finally { panel()?.closeControls() }
|
||||
}
|
||||
|
||||
@Test fun unfoldedControlsGetTheSessionBar() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
|
||||
@@ -376,7 +376,7 @@ class SessionSecondScreenTest {
|
||||
isHomeApp = false, androidApps = emptyList(), hudOn = false,
|
||||
micAvailable = micAvailable, micEnabled = micEnabled, onMicEnabled = onMic,
|
||||
frameGen = FrameGen.Mode.OFF, lossless = Lossless.State.NONE, oscMode = "never",
|
||||
suspendPolicy = "auto", backActionsInverted = false,
|
||||
onScreenButtonsVisible = false, suspendPolicy = "auto", backActionsInverted = false,
|
||||
touchMode = "auto", touchAuto = "trackpad", fexPreset = "default",
|
||||
secondScreenMode = SecondScreenMode.NONE, secondScreenDisplays = displays, selectedSecondScreenDisplay = 1,
|
||||
onHud = {}, onFrameGenPick = {}, onImportLossless = {}, onKeyboard = {}, onHardwareKeyboard = {},
|
||||
|
||||
@@ -302,7 +302,6 @@ For games that confine a visible cursor and warp it, which the automatic rule ca
|
||||
The rule only counted a lock with a cursor position hint. A game that hides the cursor and confines the
|
||||
pointer through a grab (Wine's ClipCursor) has a constraint with no hint, so the nested pointer stayed
|
||||
absolute, the constraint held the X pointer still and no raw motion was produced: the camera froze.
|
||||
0133 narrows this, and the upstream hint rule, to games that hold the X pointer.
|
||||
|
||||
## 0129 steamcompmgr, wayland: release a pointer lock at the game's pointer
|
||||
|
||||
@@ -332,15 +331,3 @@ With `--follow-game-window-mode`, the outer window of a nested SDL gamescope fol
|
||||
asking for `_NET_WM_STATE_FULLSCREEN`, or a Steam game covering the X screen without minimise or maximise functions or
|
||||
decorations in `_MOTIF_WM_HINTS`, makes the outer window fullscreen on the desktop. Anything else makes it a normal window of the
|
||||
game window's size, or a maximised one when that size does not fit the desktop with its title bar and panel. A change is applied only when the wanted mode changes, so a manual resize or Super+F stays.
|
||||
|
||||
## 0133 steamcompmgr, wlserver: a pointer constraint is relative only while the game holds the X pointer
|
||||
|
||||
A game that hides the system cursor, draws its own and grabs the pointer to its window (Stardew Valley, Balatro) gets a lock
|
||||
from Xwayland, which emulates the game's warps with a cursor position hint. Upstream kept any hinted lock relative and 0128
|
||||
added every other constraint, so gamescope held the host's pointer lock and DroidDeck turned every direct touch swipe into
|
||||
camera motion: dragging in those games only moved their cursor. Without relative mode such a lock still follows gamescope's
|
||||
relative motion and touches still place the pointer. gamescope now compares the motion it sends (seat movement, or relative
|
||||
motion while the seat is locked) with how the X pointer moves. A hinted constraint starts relative, as upstream, also when its hint arrives with the game's first warp, and is released
|
||||
after 30 moving frames on which the X pointer follows. Any constraint goes relative once the X pointer moves less than a quarter
|
||||
of that motion on three frames within half a second, as it does when the game recentres it or clips it to a small rectangle
|
||||
(PEAK). `tools/tests/test_gamescope_pointer_hold.py` compiles the rule from this patch.
|
||||
@@ -1,173 +0,0 @@
|
||||
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
|
||||
--- a/src/steamcompmgr.cpp
|
||||
+++ b/src/steamcompmgr.cpp
|
||||
@@ -3165,6 +3165,128 @@
|
||||
wlserver_unlock();
|
||||
}
|
||||
|
||||
+struct PointerHold
|
||||
+{
|
||||
+ int nAwayFrames = 0;
|
||||
+ int nFreeFrames = 0;
|
||||
+ uint64_t ulFirstAway = 0;
|
||||
+ bool bHeld = false;
|
||||
+};
|
||||
+
|
||||
+static bool pointer_hold_step( PointerHold &state, bool bConstrained, double flSeatDX, double flSeatDY, double flPointerDX, double flPointerDY, uint64_t ulNow )
|
||||
+{
|
||||
+ if ( !bConstrained )
|
||||
+ {
|
||||
+ state = PointerHold{};
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ const double flSeat = std::max( std::abs( flSeatDX ), std::abs( flSeatDY ) );
|
||||
+ if ( flSeat < 4.0 )
|
||||
+ return state.bHeld;
|
||||
+
|
||||
+ const double flMiss = std::max( std::abs( flSeatDX - flPointerDX ), std::abs( flSeatDY - flPointerDY ) );
|
||||
+ if ( flMiss > flSeat * 0.75 )
|
||||
+ {
|
||||
+ if ( state.nAwayFrames == 0 || ulNow - state.ulFirstAway > 500'000'000ul )
|
||||
+ {
|
||||
+ state.nAwayFrames = 0;
|
||||
+ state.ulFirstAway = ulNow;
|
||||
+ }
|
||||
+ state.nFreeFrames = 0;
|
||||
+ if ( ++state.nAwayFrames >= 3 )
|
||||
+ state.bHeld = true;
|
||||
+ }
|
||||
+ else if ( state.bHeld && ++state.nFreeFrames >= 30 )
|
||||
+ {
|
||||
+ state = PointerHold{};
|
||||
+ }
|
||||
+
|
||||
+ return state.bHeld;
|
||||
+}
|
||||
+
|
||||
+static bool s_bPointerHeld = false;
|
||||
+
|
||||
+static void update_pointer_hold( global_focus_t *pFocus, bool bConstrained, bool bHinted )
|
||||
+{
|
||||
+ static PointerHold s_Hold;
|
||||
+ static bool s_bTracking = false;
|
||||
+ static double s_flSeatX = 0.0;
|
||||
+ static double s_flSeatY = 0.0;
|
||||
+ static int s_nRootX = 0;
|
||||
+ static int s_nRootY = 0;
|
||||
+ static bool s_bHinted = false;
|
||||
+ static const steamcompmgr_win_t *s_pWindow = nullptr;
|
||||
+ static const struct wlr_pointer_constraint_v1 *s_pConstraint = nullptr;
|
||||
+
|
||||
+ wlserver_lock();
|
||||
+ const double flX = wlserver.mouse_surface_cursorx;
|
||||
+ const double flY = wlserver.mouse_surface_cursory;
|
||||
+ const double flSentX = wlserver.flSentDX;
|
||||
+ const double flSentY = wlserver.flSentDY;
|
||||
+ const struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint();
|
||||
+ wlserver.flSentDX = 0.0;
|
||||
+ wlserver.flSentDY = 0.0;
|
||||
+ wlserver_unlock();
|
||||
+
|
||||
+ if ( !bConstrained || !pFocus->cursor )
|
||||
+ {
|
||||
+ s_bTracking = false;
|
||||
+ s_bHinted = false;
|
||||
+ s_bPointerHeld = pointer_hold_step( s_Hold, false, 0.0, 0.0, 0.0, 0.0, 0 );
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const steamcompmgr_win_t *pWindow = pFocus->inputFocusWindow;
|
||||
+ if ( pWindow != s_pWindow || pConstraint != s_pConstraint )
|
||||
+ {
|
||||
+ s_bTracking = false;
|
||||
+ s_bHinted = false;
|
||||
+ }
|
||||
+ s_pWindow = pWindow;
|
||||
+ s_pConstraint = pConstraint;
|
||||
+
|
||||
+ if ( s_bTracking && bHinted && !s_bHinted )
|
||||
+ {
|
||||
+ s_Hold = PointerHold{};
|
||||
+ s_Hold.bHeld = true;
|
||||
+ s_bPointerHeld = true;
|
||||
+ }
|
||||
+ s_bHinted = bHinted;
|
||||
+
|
||||
+ double flDX = flX - s_flSeatX;
|
||||
+ double flDY = flY - s_flSeatY;
|
||||
+ if ( flDX == 0.0 && flDY == 0.0 )
|
||||
+ {
|
||||
+ flDX = flSentX;
|
||||
+ flDY = flSentY;
|
||||
+ }
|
||||
+
|
||||
+ if ( s_bTracking && flDX == 0.0 && flDY == 0.0 )
|
||||
+ return;
|
||||
+
|
||||
+ int nRootX = 0;
|
||||
+ int nRootY = 0;
|
||||
+ pFocus->cursor->QueryRootPosition( &nRootX, &nRootY );
|
||||
+
|
||||
+ if ( s_bTracking )
|
||||
+ {
|
||||
+ s_bPointerHeld = pointer_hold_step( s_Hold, true, flDX, flDY, nRootX - s_nRootX, nRootY - s_nRootY, get_time_in_nanos() );
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ s_Hold = PointerHold{};
|
||||
+ s_Hold.bHeld = bHinted;
|
||||
+ s_bPointerHeld = bHinted;
|
||||
+ }
|
||||
+
|
||||
+ s_bTracking = true;
|
||||
+ s_flSeatX = flX;
|
||||
+ s_flSeatY = flY;
|
||||
+ s_nRootX = nRootX;
|
||||
+ s_nRootY = nRootY;
|
||||
+}
|
||||
+
|
||||
bool ShouldDrawCursor()
|
||||
{
|
||||
if ( relative_mouse_forced() )
|
||||
@@ -11029,9 +11151,15 @@
|
||||
nRelativeMouse != 2 && !bExcludedAppId && !bHasPointerConstraint && !window_is_steam( pPaintFocus->inputFocusWindow ) );
|
||||
}
|
||||
|
||||
+ if ( vblank )
|
||||
+ update_pointer_hold( pPaintFocus, bHasPointerConstraint && bImageEmpty && nRelativeMouse != 2 && !bExcludedAppId,
|
||||
+ pPaintFocus->cursor && pPaintFocus->cursor->IsConstrained() );
|
||||
+
|
||||
+ const bool bGameHoldsPointer = bHasPointerConstraint && s_bPointerHeld;
|
||||
+
|
||||
const bool bRelativeMouseMode = nRelativeMouse == 2
|
||||
? false
|
||||
- : ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bHasPointerConstraint && !bExcludedAppId ) );
|
||||
+ : ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bGameHoldsPointer && !bExcludedAppId ) );
|
||||
|
||||
static bool s_bRelativeMouseMode = false;
|
||||
if ( s_bRelativeMouseMode && !bRelativeMouseMode && pPaintFocus->cursor &&
|
||||
diff --git a/src/wlserver.cpp b/src/wlserver.cpp
|
||||
--- a/src/wlserver.cpp
|
||||
+++ b/src/wlserver.cpp
|
||||
@@ -346,6 +346,8 @@
|
||||
{
|
||||
assert( wlserver_is_lock_held() );
|
||||
|
||||
+ wlserver.flSentDX += unaccel_dx;
|
||||
+ wlserver.flSentDY += unaccel_dy;
|
||||
wlr_relative_pointer_manager_v1_send_relative_motion( wlserver.relative_pointer_manager, wlserver.wlr.seat, 0, unaccel_dx, unaccel_dy, unaccel_dx, unaccel_dy );
|
||||
}
|
||||
|
||||
diff --git a/src/wlserver.hpp b/src/wlserver.hpp
|
||||
--- a/src/wlserver.hpp
|
||||
+++ b/src/wlserver.hpp
|
||||
@@ -171,6 +171,8 @@
|
||||
bool bFollowClientPointer = false;
|
||||
double flFollowDX = 0.0;
|
||||
double flFollowDY = 0.0;
|
||||
+ double flSentDX = 0.0;
|
||||
+ double flSentDY = 0.0;
|
||||
|
||||
bool button_held[ WLSERVER_BUTTON_COUNT ];
|
||||
std::set <uint32_t> touch_down_ids;
|
||||
@@ -1078,8 +1078,7 @@ class Helper:
|
||||
wanted = COMPAT["requested_tool"](request, catalog)
|
||||
newer = isinstance(request.get("seq"), int) and request["seq"] > int(state.get("applied", 0) or 0)
|
||||
waiting = newer and wanted is not None and wanted not in self.known
|
||||
default, state = COMPAT["choose_default"](top["selected"], state, request, catalog, apply_request=not waiting,
|
||||
listed=[t["name"] for t in live])
|
||||
default, state = COMPAT["choose_default"](top["selected"], state, request, live, apply_request=not waiting)
|
||||
installed = COMPAT["installed_apps"](self.steam_root)
|
||||
self.installed = installed
|
||||
picks = COMPAT["valve_picks"](self.steam_root)
|
||||
@@ -1138,7 +1137,7 @@ class Helper:
|
||||
state.update(
|
||||
auto={app: tool for app, tool in auto.items() if tool is not COMPAT["NATIVE_CHOICE"]},
|
||||
native_choices=sorted(refused_native | {app for app, tool in auto.items() if tool is COMPAT["NATIVE_CHOICE"]}),
|
||||
picked=COMPAT["picked_after"](current, changes), tools=catalog, live=True, written=default,
|
||||
picked=COMPAT["picked_after"](current, changes), tools=catalog, live=True,
|
||||
games_applied=COMPAT["applied_game_requests"](requests, overrides, state))
|
||||
state.pop("native", None)
|
||||
if waiting:
|
||||
|
||||
@@ -355,9 +355,8 @@ def main():
|
||||
"linux_tool": "droiddeck-direct"}
|
||||
else:
|
||||
protect = compat.adopt_extras(tools)
|
||||
owned = [str(int(g["appid"]) & 0xFFFFFFFF) for g in games if g.get("exe") and not g.get("native")]
|
||||
used = compat.register_default(cfg, apps=owned + [str(appid) for appid in set(routes.values())],
|
||||
protect=protect, owned=owned, **own)
|
||||
used = compat.register_default(cfg, apps=[str(int(g["appid"]) & 0xFFFFFFFF) for g in games if g.get("exe") and not g.get("native")] + [str(appid) for appid in set(routes.values())],
|
||||
protect=protect, **own)
|
||||
print("droiddeck-steam-shortcuts: mapped them to %s" % used)
|
||||
except Exception as e:
|
||||
print("droiddeck-steam-shortcuts: compat mapping skipped (%s)" % e)
|
||||
|
||||
@@ -1307,13 +1307,9 @@ def now_ms():
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def choose_default(steam_default, state, request, catalog, adopt_steam=True, apply_request=True, now=None, listed=None):
|
||||
def choose_default(steam_default, state, request, catalog, adopt_steam=True, apply_request=True, now=None):
|
||||
state = dict(state)
|
||||
names = [t["name"] for t in catalog]
|
||||
listed = names if listed is None else listed
|
||||
if "written" not in state and state.get("source") == "steam":
|
||||
state["source"] = "fallback"
|
||||
hidden = state.get("default") in names and state.get("default") not in listed
|
||||
seq = request.get("seq")
|
||||
seq = seq if isinstance(seq, int) else 0
|
||||
if apply_request and seq > int(state.get("applied", 0) or 0):
|
||||
@@ -1325,12 +1321,11 @@ def choose_default(steam_default, state, request, catalog, adopt_steam=True, app
|
||||
else:
|
||||
print("steam-compatibility: the app asked for %s, which is not installed; kept %s" % (
|
||||
request.get("name") or request.get("dir"), state.get("default") or TOOL))
|
||||
elif adopt_steam and steam_default in names and not hidden and steam_default not in (
|
||||
state.get("default"), state.get("written", state.get("default") or TOOL)):
|
||||
elif adopt_steam and steam_default in names and steam_default != state.get("default"):
|
||||
state.update(default=steam_default, seq=now or now_ms(), source="steam")
|
||||
if state.get("default") not in names and state.get("default") != TOOL:
|
||||
state.update(default=TOOL, source="fallback")
|
||||
return state["default"] if state["default"] in listed else TOOL, state
|
||||
if state.get("default") not in names:
|
||||
state["default"] = TOOL
|
||||
return state["default"], state
|
||||
|
||||
|
||||
def mapping_block(tokens):
|
||||
@@ -1487,7 +1482,7 @@ def live_helper_running(directory=None):
|
||||
|
||||
|
||||
def register_default(config_path, apps=(), protect=(), default=None, catalog=None, directory=None, settle=False, picks=(),
|
||||
linux_tool=FEX_TOOL, owned=None):
|
||||
linux_tool=FEX_TOOL):
|
||||
with locked(directory):
|
||||
state = read_json(state_path("state.json", directory))
|
||||
with open(config_path, encoding="utf-8", errors="replace") as f:
|
||||
@@ -1502,24 +1497,18 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non
|
||||
dict(protect) if isinstance(protect, dict) else {name: name for name in protect})
|
||||
names = [t["name"] for t in catalog]
|
||||
newest, gone = own_proton_moves(state, catalog)
|
||||
previous = state.get("default")
|
||||
if owned is not None:
|
||||
auto = followers(state, current)
|
||||
known = {app for app in state.get("owned", ()) if isinstance(app, str)} if isinstance(state.get("owned"), list) else set()
|
||||
pinned = {app: current[app] for app in set(owned) - known
|
||||
if current.get(app) in {TOOL, previous} | gone and app not in auto}
|
||||
if pinned and isinstance(auto, dict):
|
||||
state["auto"] = dict(auto, **pinned)
|
||||
state["owned"] = sorted(set(owned), key=app_order)
|
||||
previous, applied, chosen = state.get("default"), state.get("applied"), state.get("seq")
|
||||
if default is None:
|
||||
if settle:
|
||||
default, state = choose_default(current.get("0", ""), state, read_json(state_path("request.json", directory)), catalog)
|
||||
else:
|
||||
default = state.get("default") if state.get("default") in names else TOOL
|
||||
if settle and newest and default == TOOL and state.get("source") not in ("app", "steam"):
|
||||
if settle and newest and default == TOOL and state.get("applied") == applied and state.get("seq") == chosen and (
|
||||
previous in gone or not state.get("own_proton")):
|
||||
default = newest
|
||||
state.update(default=newest, seq=now_ms(), source="update")
|
||||
state.pop("own_proton", None)
|
||||
if settle and newest:
|
||||
state["own_proton"] = True
|
||||
if newest:
|
||||
replacements = {app: newest for app, name in current.items() if app != "0" and name in gone}
|
||||
set_mappings(tokens, mapping, replacements)
|
||||
@@ -1550,8 +1539,6 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non
|
||||
state.pop("native", None)
|
||||
state["tools"] = catalog
|
||||
state.setdefault("default", default)
|
||||
if settle:
|
||||
state["written"] = default
|
||||
write_json(state_path("state.json", directory), state)
|
||||
return default
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import runpy
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin"
|
||||
COMPAT = runpy.run_path(str(BIN / "steam-compatibility"))
|
||||
@@ -12,8 +11,6 @@ OLD = {"name": "DroidDeck-Proton-11-11.2", "display": "DroidDeck-Proton 11-11.2"
|
||||
NEW = {"name": "DroidDeck-Proton-11-11.4", "display": "DroidDeck-Proton 11-11.4", "dir": "DroidDeck-Proton 11-11.4", "valve": False}
|
||||
VALVE = {"name": TOOL, "display": "Proton Experimental ARM64", "dir": "Proton - Experimental (ARM64)", "valve": True}
|
||||
GE = {"name": "GE-Proton11-7", "display": "GE-Proton11-7", "dir": "GE-Proton11-7", "valve": False}
|
||||
GE10 = {"name": "GE-Proton10-3", "display": "GE-Proton10-3", "dir": "GE-Proton10-3", "valve": False}
|
||||
NEWER = {"name": "DroidDeck-Proton-11-12.1", "display": "DroidDeck-Proton 11-12.1", "dir": "DroidDeck-Proton 11-12.1", "valve": False}
|
||||
|
||||
|
||||
def config_text(entries):
|
||||
@@ -22,19 +19,15 @@ def config_text(entries):
|
||||
|
||||
|
||||
class DroidDeckProtonDefaultTest(unittest.TestCase):
|
||||
def run_default(self, state, mapping, catalog, apps=(), request=None, settle=True, owned=None, games=None):
|
||||
def run_default(self, state, mapping, catalog, apps=(), request=None, settle=True):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
directory = Path(tmp)
|
||||
(directory / "state.json").write_text(json.dumps(state))
|
||||
if request is not None:
|
||||
(directory / "request.json").write_text(json.dumps(request))
|
||||
if games is not None:
|
||||
(directory / "games.json").write_text(json.dumps({"version": 1, "games": games}))
|
||||
config = directory / "config.vdf"
|
||||
config.write_text(config_text(mapping))
|
||||
with mock.patch.dict(COMPAT["register_default"].__globals__, steam_running=lambda: False):
|
||||
default = COMPAT["register_default"](str(config), list(apps), catalog=catalog, directory=str(directory),
|
||||
settle=settle, owned=owned)
|
||||
default = COMPAT["register_default"](str(config), list(apps), catalog=catalog, directory=str(directory), settle=settle)
|
||||
return default, COMPAT["read_config_mapping"](str(config)), json.loads((directory / "state.json").read_text())
|
||||
|
||||
def test_a_replacement_seen_while_steam_runs_still_moves_the_default_later(self):
|
||||
@@ -57,7 +50,8 @@ class DroidDeckProtonDefaultTest(unittest.TestCase):
|
||||
{"default": TOOL, "auto": {"42": TOOL}}, [("0", TOOL), ("42", TOOL), ("43", TOOL)], [VALVE, NEW], ["42", "43"])
|
||||
self.assertEqual(default, NEW["name"])
|
||||
self.assertEqual(mapping, {"0": NEW["name"], "42": NEW["name"], "43": TOOL})
|
||||
self.assertEqual((state["default"], state["source"], state["written"]), (NEW["name"], "update", NEW["name"]))
|
||||
self.assertEqual(state["default"], NEW["name"])
|
||||
self.assertTrue(state["own_proton"])
|
||||
|
||||
def test_a_default_picked_after_the_switch_is_kept(self):
|
||||
default, mapping, state = self.run_default(
|
||||
@@ -65,7 +59,8 @@ class DroidDeckProtonDefaultTest(unittest.TestCase):
|
||||
[("0", NEW["name"])], [VALVE, NEW], request={"seq": 2, "valve": True, "dir": VALVE["dir"]})
|
||||
self.assertEqual(default, TOOL)
|
||||
self.assertEqual(mapping, {"0": TOOL})
|
||||
default, mapping, state = self.run_default(state, [("0", TOOL)], [VALVE, NEW])
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": TOOL, "own_proton": True, "tools": [VALVE, NEW]}, [("0", TOOL)], [VALVE, NEW])
|
||||
self.assertEqual(default, TOOL)
|
||||
self.assertEqual(mapping, {"0": TOOL})
|
||||
|
||||
@@ -80,7 +75,7 @@ class DroidDeckProtonDefaultTest(unittest.TestCase):
|
||||
|
||||
def test_a_replaced_build_does_not_override_a_valve_default(self):
|
||||
default, mapping, _ = self.run_default(
|
||||
{"default": TOOL, "source": "app", "written": TOOL, "tools": [VALVE, OLD]}, [("0", TOOL), ("43", OLD["name"])], [VALVE, NEW], ["43"])
|
||||
{"default": TOOL, "own_proton": True, "tools": [VALVE, OLD]}, [("0", TOOL), ("43", OLD["name"])], [VALVE, NEW], ["43"])
|
||||
self.assertEqual(default, TOOL)
|
||||
self.assertEqual(mapping, {"0": TOOL, "43": NEW["name"]})
|
||||
|
||||
@@ -90,80 +85,6 @@ class DroidDeckProtonDefaultTest(unittest.TestCase):
|
||||
self.assertEqual(mapping, {"0": TOOL})
|
||||
self.assertNotIn("own_proton", state)
|
||||
|
||||
def test_a_fresh_state_with_steam_on_proton_experimental_moves_to_the_newest_build(self):
|
||||
default, mapping, state = self.run_default({}, [("0", TOOL)], [VALVE, OLD, NEW])
|
||||
self.assertEqual((default, mapping), (NEW["name"], {"0": NEW["name"]}))
|
||||
default, mapping, state = self.run_default(state, [("0", NEW["name"])], [VALVE, OLD, NEW])
|
||||
self.assertEqual((default, state["source"]), (NEW["name"], "update"))
|
||||
|
||||
def test_a_default_adopted_from_steam_by_an_earlier_build_recovers(self):
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": TOOL, "source": "steam", "seq": 5, "own_proton": True, "tools": [VALVE, NEW]}, [("0", TOOL)], [VALVE, NEW])
|
||||
self.assertEqual((default, mapping, state["source"]), (NEW["name"], {"0": NEW["name"]}, "update"))
|
||||
self.assertNotIn("own_proton", state)
|
||||
|
||||
def test_a_request_applied_without_a_change_still_moves_to_the_newest_build(self):
|
||||
for request in ({"seq": 5, "valve": True, "dir": VALVE["dir"]}, {"seq": 5, "valve": False, "dir": GE["dir"]}):
|
||||
default, mapping, state = self.run_default({"default": TOOL, "applied": 1}, [("0", TOOL)], [VALVE, NEW], request=request)
|
||||
self.assertEqual((default, mapping, state["applied"]), (NEW["name"], {"0": NEW["name"]}, 5))
|
||||
|
||||
def test_a_default_that_is_not_installed_falls_back_without_counting_as_a_pick(self):
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": GE["name"], "source": "app", "seq": 9, "written": GE["name"]}, [("0", GE["name"])], [VALVE])
|
||||
self.assertEqual((default, mapping), (TOOL, {"0": TOOL}))
|
||||
self.assertEqual((state["source"], state["seq"]), ("fallback", 9))
|
||||
default, _, state = self.run_default(state, [("0", TOOL)], [VALVE, NEW])
|
||||
self.assertEqual((default, state["source"]), (NEW["name"], "update"))
|
||||
|
||||
def test_proton_experimental_asked_for_in_the_app_stays(self):
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": NEW["name"], "written": NEW["name"], "applied": 1}, [("0", NEW["name"])], [VALVE, NEW],
|
||||
request={"seq": 2, "valve": True, "dir": VALVE["dir"]})
|
||||
self.assertEqual((default, state["source"]), (TOOL, "app"))
|
||||
for catalog in ([VALVE, NEW], [VALVE, NEW, NEWER], [VALVE, NEWER]):
|
||||
default, mapping, state = self.run_default(state, [("0", TOOL)], catalog)
|
||||
self.assertEqual((default, mapping, state["source"]), (TOOL, {"0": TOOL}, "app"))
|
||||
|
||||
def test_proton_experimental_picked_in_steam_stays(self):
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": NEW["name"], "written": NEW["name"], "source": "update"}, [("0", TOOL)], [VALVE, NEW])
|
||||
self.assertEqual((default, state["source"], state["written"]), (TOOL, "steam", TOOL))
|
||||
for catalog in ([VALVE, NEW], [VALVE, NEW, NEWER]):
|
||||
default, mapping, state = self.run_default(state, [("0", TOOL)], catalog)
|
||||
self.assertEqual((default, mapping, state["source"]), (TOOL, {"0": TOOL}, "steam"))
|
||||
|
||||
def test_steam_echoing_the_written_default_is_not_a_pick(self):
|
||||
default, _, state = self.run_default(
|
||||
{"default": NEW["name"], "written": TOOL, "source": "update"}, [("0", TOOL)], [VALVE, NEW])
|
||||
self.assertEqual((default, state["source"]), (NEW["name"], "update"))
|
||||
|
||||
def test_a_default_the_running_client_does_not_list_is_kept(self):
|
||||
state = {"default": NEW["name"], "written": NEW["name"], "seq": 1, "source": "update"}
|
||||
default, state = COMPAT["choose_default"](TOOL, state, {}, [VALVE, NEW], listed=[TOOL])
|
||||
self.assertEqual((default, state["default"], state["source"], state["seq"]), (TOOL, NEW["name"], "update", 1))
|
||||
default, state = COMPAT["choose_default"](GE["name"], dict(state, written=TOOL), {}, [VALVE, NEW, GE])
|
||||
self.assertEqual((default, state["source"]), (GE["name"], "steam"))
|
||||
|
||||
def test_owned_shortcuts_follow_the_default_and_explicit_picks_stay(self):
|
||||
owned = ["3000000001", "3000000002", "3000000003", "3000000004"]
|
||||
catalog = [VALVE, NEW, GE, GE10]
|
||||
default, mapping, state = self.run_default(
|
||||
{"default": NEW["name"], "written": NEW["name"], "source": "update", "auto": {}},
|
||||
[("0", NEW["name"]), ("3000000001", NEW["name"]), ("3000000002", TOOL), ("3000000003", GE["name"]),
|
||||
("3000000004", NEW["name"]), ("42", GE["name"]), ("44", NEW["name"])],
|
||||
catalog, ["42", "44"] + owned, settle=False, owned=owned, games={"44": {"valve": False, "dir": GE["dir"]}})
|
||||
self.assertEqual(mapping, {"0": NEW["name"], "3000000001": NEW["name"], "3000000002": NEW["name"],
|
||||
"3000000003": GE["name"], "3000000004": NEW["name"], "42": GE["name"], "44": GE["name"]})
|
||||
mapping["3000000001"] = TOOL
|
||||
default, mapping, state = self.run_default(state, sorted(mapping.items()), catalog, ["42", "44"] + owned,
|
||||
settle=False, owned=owned)
|
||||
self.assertEqual(mapping["3000000001"], TOOL)
|
||||
default, mapping, state = self.run_default(state, sorted(mapping.items()), catalog, ["42", "44"] + owned,
|
||||
request={"seq": 1, "valve": False, "dir": GE10["dir"]})
|
||||
self.assertEqual(default, GE10["name"])
|
||||
self.assertEqual(mapping, {"0": GE10["name"], "3000000001": TOOL, "3000000002": GE10["name"],
|
||||
"3000000003": GE["name"], "3000000004": GE10["name"], "42": GE["name"], "44": GE["name"]})
|
||||
|
||||
def test_the_newest_build_is_found_by_version(self):
|
||||
newest, gone = COMPAT["own_proton_moves"]({"tools": [OLD]}, [dict(NEW, name="a", dir="DroidDeck-Proton 11-9.10"), dict(NEW, name="b", dir="DroidDeck-Proton 11-11.1")])
|
||||
self.assertEqual(newest, "b")
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PATCH = ROOT / "tools/gamescope/patches/0133-constraint-relative-when-held.patch"
|
||||
|
||||
|
||||
def added_source():
|
||||
return "\n".join(line[1:] for line in PATCH.read_text().splitlines() if line.startswith("+") and not line.startswith("+++"))
|
||||
|
||||
|
||||
def block(text, start):
|
||||
begin = text.index(start)
|
||||
return text[begin:text.index("\n}", begin) + 3]
|
||||
|
||||
|
||||
def rules():
|
||||
text = added_source()
|
||||
return "\n".join([
|
||||
block(text, "struct PointerHold"),
|
||||
block(text, "static bool pointer_hold_step"),
|
||||
])
|
||||
|
||||
|
||||
PREAMBLE = r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
static const uint64_t FRAME = 16'666'667ul;
|
||||
|
||||
static bool run(PointerHold &state, int frames, double seatDX, double seatDY, double pointerDX, double pointerDY, uint64_t &now, bool constrained = true) {
|
||||
bool held = state.bHeld;
|
||||
for (int i = 0; i < frames; i++) {
|
||||
now += FRAME;
|
||||
held = pointer_hold_step(state, constrained, seatDX, seatDY, pointerDX, pointerDY, now);
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
int main() {
|
||||
uint64_t now = 1'000'000'000ul;
|
||||
|
||||
PointerHold cursorGame;
|
||||
assert(!run(cursorGame, 600, 12.0, -9.0, 12.0, -9.0, now));
|
||||
assert(!run(cursorGame, 600, 40.0, 3.0, 39.0, 3.0, now));
|
||||
assert(!run(cursorGame, 1, 30.0, 0.0, 0.0, 0.0, now));
|
||||
assert(!run(cursorGame, 1, 30.0, 0.0, 30.0, 0.0, now));
|
||||
assert(!run(cursorGame, 1, 30.0, 0.0, 0.0, 0.0, now));
|
||||
assert(!run(cursorGame, 120, 2.0, 1.0, 0.0, 0.0, now));
|
||||
|
||||
PointerHold offset;
|
||||
assert(!run(offset, 600, 25.0, 25.0, 25.0, 25.0, now));
|
||||
|
||||
PointerHold recentring;
|
||||
assert(!run(recentring, 2, -40.0, 12.0, 0.0, 0.0, now));
|
||||
assert(run(recentring, 1, -40.0, 12.0, 1.0, 0.0, now));
|
||||
assert(run(recentring, 600, 0.0, 0.0, 0.0, 0.0, now));
|
||||
assert(run(recentring, 29, 10.0, 4.0, 10.0, 4.0, now));
|
||||
assert(run(recentring, 1, 20.0, 0.0, 0.0, 0.0, now));
|
||||
assert(run(recentring, 29, 10.0, 4.0, 10.0, 4.0, now));
|
||||
assert(!run(recentring, 1, 10.0, 4.0, 10.0, 4.0, now));
|
||||
|
||||
PointerHold clipped;
|
||||
assert(run(clipped, 3, 0.0, 30.0, 0.0, 2.0, now));
|
||||
|
||||
PointerHold spaced;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assert(!run(spaced, 1, 50.0, 0.0, 0.0, 0.0, now));
|
||||
now += 600'000'000ul;
|
||||
}
|
||||
|
||||
PointerHold hinted;
|
||||
hinted.bHeld = true;
|
||||
assert(run(hinted, 29, 0.0, 15.0, 0.0, 15.0, now));
|
||||
assert(!run(hinted, 1, 0.0, 15.0, 0.0, 15.0, now));
|
||||
|
||||
PointerHold hintedCamera;
|
||||
hintedCamera.bHeld = true;
|
||||
assert(run(hintedCamera, 600, 30.0, -8.0, 0.0, 0.0, now));
|
||||
|
||||
PointerHold chained;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
assert(!run(chained, 1, 50.0, 0.0, 0.0, 0.0, now));
|
||||
now += 400'000'000ul;
|
||||
}
|
||||
|
||||
PointerHold released;
|
||||
assert(run(released, 3, 0.0, -25.0, 0.0, 0.0, now));
|
||||
assert(!run(released, 1, 0.0, -25.0, 0.0, 0.0, now, false));
|
||||
assert(!run(released, 2, 0.0, -25.0, 0.0, 0.0, now));
|
||||
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("c++"), "needs a C++ compiler")
|
||||
class PointerHoldTest(unittest.TestCase):
|
||||
def test_only_a_game_that_holds_the_x_pointer_goes_relative(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
source = Path(tmp) / "hold.cpp"
|
||||
binary = Path(tmp) / "hold"
|
||||
source.write_text(PREAMBLE + rules() + MAIN)
|
||||
subprocess.run(["c++", "-std=c++17", "-Wall", "-Wextra", "-Werror", str(source), "-o", str(binary)], check=True)
|
||||
self.assertEqual(subprocess.run([str(binary)], capture_output=True, text=True, check=True).stdout.strip(), "ok")
|
||||
|
||||
def test_a_hinted_constraint_starts_relative_and_others_wait_for_the_hold(self):
|
||||
added = [line.strip() for line in added_source().splitlines()]
|
||||
self.assertIn("update_pointer_hold( pPaintFocus, bHasPointerConstraint && bImageEmpty && nRelativeMouse != 2 && !bExcludedAppId,", added)
|
||||
self.assertIn("pPaintFocus->cursor && pPaintFocus->cursor->IsConstrained() );", added)
|
||||
self.assertIn("s_Hold.bHeld = bHinted;", added)
|
||||
self.assertIn("if ( s_bTracking && bHinted && !s_bHinted )", added)
|
||||
self.assertIn("const bool bGameHoldsPointer = bHasPointerConstraint && s_bPointerHeld;", added)
|
||||
self.assertIn(": ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bGameHoldsPointer && !bExcludedAppId ) );", added)
|
||||
|
||||
def test_a_new_window_or_constraint_starts_its_own_hold(self):
|
||||
tracker = block(added_source(), "static void update_pointer_hold")
|
||||
reset = tracker.index("if ( pWindow != s_pWindow || pConstraint != s_pConstraint )")
|
||||
self.assertIn("const steamcompmgr_win_t *pWindow = pFocus->inputFocusWindow;", tracker)
|
||||
constraint = tracker.index("wlserver.GetCursorConstraint();")
|
||||
self.assertLess(tracker.index("wlserver_lock();"), constraint)
|
||||
self.assertLess(constraint, tracker.index("wlserver_unlock();"))
|
||||
self.assertLess(reset, tracker.index("if ( s_bTracking && bHinted && !s_bHinted )"))
|
||||
self.assertIn("s_bTracking = false;", tracker[reset:reset + 120])
|
||||
|
||||
def test_the_motion_sent_to_clients_is_counted(self):
|
||||
added = [line.strip() for line in added_source().splitlines()]
|
||||
self.assertIn("wlserver.flSentDX += unaccel_dx;", added)
|
||||
self.assertIn("wlserver.flSentDY += unaccel_dy;", added)
|
||||
|
||||
def test_the_forced_and_follow_rules_are_unchanged(self):
|
||||
removed = [line[1:].strip() for line in PATCH.read_text().splitlines() if line.startswith("-") and not line.startswith("---")]
|
||||
self.assertEqual(removed, [": ( bForcedRelativeMouse || s_bFollowClientPointer || ( bImageEmpty && bHasPointerConstraint && !bExcludedAppId ) );"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -749,26 +749,6 @@ class ReconcileTest(unittest.TestCase):
|
||||
self.assertIn(entry % ("10", COMPAT["FEX_TOOL"]), " ".join(config.read_text().split()))
|
||||
self.assertEqual(self.games(), {})
|
||||
|
||||
def test_our_default_the_client_does_not_list_survives_to_the_next_start(self):
|
||||
build = "DroidDeck-Proton 11-11.4"
|
||||
(self.steam / "compatibilitytools.d" / build).mkdir(parents=True)
|
||||
(self.steam / "compatibilitytools.d" / build / COMPAT["EXTRA_WRAPPER"]).write_text("")
|
||||
(self.state / "state.json").write_text(json.dumps({"default": build, "written": build, "source": "update", "auto": {"10": build}}))
|
||||
page = FakePage({"0": TOOL, "10": build}, [TOOL, TOOL_11])
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual((state["default"], state["source"], state["written"]), (build, "update", TOOL))
|
||||
self.assertEqual(page.sets, [("10", TOOL), ("11", TOOL)])
|
||||
config = self.steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
vdf = '"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }'
|
||||
entry = '"%s" { "name" "%s" "config" "" "priority" "250" }'
|
||||
config.write_text(vdf % "".join(entry % (app, page.mapping[app]) for app in ("0", "10", "11")))
|
||||
with mock.patch.dict(COMPAT["register_default"].__globals__, steam_running=lambda: False):
|
||||
default = COMPAT["register_default"](str(config), ["10", "11"], catalog=COMPAT["tool_catalog"](str(self.steam)),
|
||||
directory=str(self.state), settle=True)
|
||||
self.assertEqual(default, build)
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": build, "10": build, "11": build})
|
||||
|
||||
def test_file_mode_defers_to_the_live_helper(self):
|
||||
(self.state / "live.pid").write_text("%d\n" % os.getpid())
|
||||
self.assertTrue(COMPAT["live_helper_running"]())
|
||||
|
||||
Reference in new issue
Block a user