mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Android: make the manager acknowledge dialogs pad-navigable
Seven sites across six manager screens, all the same shape: something went wrong (or finished), here is the text, press OK. Every one was an AlertDialog, so every one was its own focused Android window and killed the pad for as long as it was up — on the screens where a pad user is most likely to be stuck, since these are what an import failure or a bad path actually surfaces. They collapse to one call each against a new NotifyOverlay: the same card as the confirmation, one button instead of two, and a scrolling height-capped body so a long error can be read to the end. The window dialogs simply clipped it. The settings info panel folds onto the same thing, deleting the copy the previous commit had to make of the card. A setting description and an error notice are the same object; keeping them as one is what stops the next fix landing on only one of them, which is the exact failure that commit had to repair between the two info-hint copies. Worth reading carefully in the memory-card diff: its message dialog registered controllerFocusable ids for its OK button. That looks like working controller code and never was — it registered from inside a dialog window, so the ids landed in the registry, reported positions the pad behind could navigate onto, and answered nothing. Removing them is the fix, not a loss of function. These are only reachable by causing the error they report, so most are verified by inspection rather than exercised.
This commit is contained in:
+5
-5
@@ -128,11 +128,11 @@ fun AchievementsScreen(onBack: () -> Unit, viewModel: AchievementsViewModel = vi
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissError,
|
||||
title = { Text("RetroAchievements") },
|
||||
text = { Text(error) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissError) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = "RetroAchievements",
|
||||
message = error,
|
||||
onDismiss = viewModel::dismissError,
|
||||
idPrefix = "ra.error",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -147,11 +147,11 @@ fun BiosManagerScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Bio
|
||||
)
|
||||
}
|
||||
state.error?.let { error ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissError,
|
||||
title = { Text(str("setup.page.bios.title")) },
|
||||
text = { Text(error) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissError) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = str("setup.page.bios.title"),
|
||||
message = error,
|
||||
onDismiss = viewModel::dismissError,
|
||||
idPrefix = "bios.error",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.armsx2.ui.common
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
@@ -102,6 +105,71 @@ fun ConfirmOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A one-button acknowledgement — an error, a result, an explanation. The other half of the pair
|
||||
* with [ConfirmOverlay]: same card, no choice to make.
|
||||
*
|
||||
* The body scrolls and is height-capped, and the modal declares that scroll state, so a message
|
||||
* longer than the panel can be read with the pad's Up/Down once the selection has nowhere left to
|
||||
* go. Every window dialog this replaces simply clipped long text with no way to reach the rest.
|
||||
*
|
||||
* @param idPrefix distinguishes concurrent notices' nav ids and layer.
|
||||
*/
|
||||
@Composable
|
||||
fun NotifyOverlay(
|
||||
title: String,
|
||||
message: String,
|
||||
onDismiss: () -> Unit,
|
||||
buttonLabel: String = str("action.ok"),
|
||||
idPrefix: String = "notify",
|
||||
) {
|
||||
val layer = "notify-overlay:$idPrefix"
|
||||
val bodyScroll = rememberScrollState()
|
||||
PadModal(
|
||||
key = layer,
|
||||
onDismiss = onDismiss,
|
||||
initialFocusId = "$layer.ok",
|
||||
scrollState = bodyScroll,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.widthIn(max = 420.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)),
|
||||
tonalElevation = 6.dp,
|
||||
) {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.heightIn(max = 340.dp)
|
||||
.verticalScroll(bodyScroll),
|
||||
)
|
||||
Spacer(Modifier.height(18.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
ConfirmButton(
|
||||
label = buttonLabel,
|
||||
id = "$layer.ok",
|
||||
onClick = onDismiss,
|
||||
container = MaterialTheme.colorScheme.primaryContainer,
|
||||
content = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* App-wide confirmation host, for prompts raised from code that has no composition of its own to
|
||||
* put a [ConfirmOverlay] in — a click handler deep in a view model, say.
|
||||
|
||||
@@ -131,11 +131,11 @@ fun MemoryCardScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Memo
|
||||
)
|
||||
}
|
||||
(state.error ?: state.message)?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissMessage,
|
||||
title = { Text(if (state.error != null) str("memcard.title") else str("action.ok")) },
|
||||
text = { Text(message) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissMessage, modifier = Modifier.controllerFocusable("memcard.message.ok", onConfirm = viewModel::dismissMessage)) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = if (state.error != null) str("memcard.title") else str("action.ok"),
|
||||
message = message,
|
||||
onDismiss = viewModel::dismissMessage,
|
||||
idPrefix = "memcard.message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -114,11 +113,11 @@ fun PatchManagerScreen(onBack: () -> Unit, game: GameInfo? = null, viewModel: Pa
|
||||
com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN)
|
||||
onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) }
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissMessage,
|
||||
title = { Text(if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats")) },
|
||||
text = { Text(message) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats"),
|
||||
message = message,
|
||||
onDismiss = viewModel::dismissMessage,
|
||||
idPrefix = "patches.message",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -269,11 +268,11 @@ fun PatchesSettingsTab(game: GameInfo? = null, viewModel: PatchManagerViewModel
|
||||
com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN)
|
||||
onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) }
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissMessage,
|
||||
title = { Text(if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats")) },
|
||||
text = { Text(message) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = if (state.error == null) str("action.ok") else str("patches.dialog.patchesAndCheats"),
|
||||
message = message,
|
||||
onDismiss = viewModel::dismissMessage,
|
||||
idPrefix = "patches.message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
@@ -99,13 +98,11 @@ fun SaveManagerScreen(onBack: () -> Unit, viewModel: SaveManagerViewModel = view
|
||||
}
|
||||
|
||||
(state.error ?: state.message)?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissMessage,
|
||||
title = { Text(str("savestate.title.loadManage")) },
|
||||
text = { Text(message) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) }
|
||||
},
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = str("savestate.title.loadManage"),
|
||||
message = message,
|
||||
onDismiss = viewModel::dismissMessage,
|
||||
idPrefix = "saves.message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,67 +1272,15 @@ internal fun InfoHint(title: String, message: String) {
|
||||
com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_OPEN)
|
||||
onDispose { com.armsx2.MenuSfx.play(com.armsx2.MenuSfx.Event.POPUP_CLOSE) }
|
||||
}
|
||||
val layer = "info-hint:$title"
|
||||
val bodyScroll = rememberScrollState()
|
||||
com.armsx2.ui.common.PadModal(
|
||||
key = layer,
|
||||
// The shared acknowledgement panel: same card, same scrolling body, so a setting
|
||||
// description behaves exactly like an error notice and neither can drift from the other.
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = title,
|
||||
message = message,
|
||||
onDismiss = { open = false },
|
||||
// Every setting description is a candidate for being longer than the panel, so the
|
||||
// pad needs Up/Down to read it — Close is the only thing here to select.
|
||||
scrollState = bodyScroll,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.widthIn(max = 420.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)),
|
||||
tonalElevation = 6.dp,
|
||||
) {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
// Cap the height and scroll INSIDE it. AlertDialog did not scroll its text
|
||||
// slot at all, so a description longer than the slot was CLIPPED mid-sentence
|
||||
// with no way to reach the rest — which is most of the longer setting
|
||||
// explanations (reported against Low Latency Mode, cut off at "...turning
|
||||
// back off if the frame pacing").
|
||||
Text(
|
||||
message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.heightIn(max = 340.dp)
|
||||
.verticalScroll(bodyScroll),
|
||||
)
|
||||
Spacer(Modifier.height(18.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
Surface(
|
||||
onClick = { open = false },
|
||||
modifier = Modifier.controllerFocusable(
|
||||
controllerId = "$layer.close",
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
onConfirm = { open = false },
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
Text(
|
||||
str("action.close"),
|
||||
modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buttonLabel = str("action.close"),
|
||||
idPrefix = "info:$title",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -96,11 +95,11 @@ fun TextureManagerScreen(onBack: () -> Unit, viewModel: TextureManagerViewModel
|
||||
}
|
||||
}
|
||||
(state.error ?: state.message)?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissMessage,
|
||||
title = { Text(if (state.error == null) str("action.ok") else str("renderer.section.texturePacks")) },
|
||||
text = { Text(message) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissMessage) { Text(str("action.ok")) } },
|
||||
com.armsx2.ui.common.NotifyOverlay(
|
||||
title = if (state.error == null) str("action.ok") else str("renderer.section.texturePacks"),
|
||||
message = message,
|
||||
onDismiss = viewModel::dismissMessage,
|
||||
idPrefix = "textures.message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user