fix(android): settings dropdowns trapped D-pad/controller focus
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m20s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 30s
android / android (push) Successful in 3m29s
deb / build-publish (push) Successful in 3m5s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m43s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m8s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 7m20s
docker / deploy-docs (push) Successful in 6s
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m20s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 30s
android / android (push) Successful in 3m29s
deb / build-publish (push) Successful in 3m5s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m43s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m8s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 7m20s
docker / deploy-docs (push) Successful in 6s
ExposedDropdownMenuBox anchors on a read-only OutlinedTextField, and a text field captures D-pad focus -- directional keys never escape it, so on a TV/controller you got stuck on the first select. Replace SettingDropdown with a clickable Surface + DropdownMenu (no text field): D-pad moves between settings, A opens the menu, A selects an item. Adds a primary-colour focus border so the focused setting reads across a room. Verified locally: ./gradlew :app:assembleDebug BUILD SUCCESSFUL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,9 @@ import android.content.pm.PackageManager
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -14,14 +16,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -31,6 +33,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -171,8 +174,12 @@ private fun ToggleRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A labelled read-only dropdown over [options] (value → label); calls [onSelect] on a pick. */
|
/**
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
* A labelled value that opens a menu on click. Uses a clickable [Surface] + [DropdownMenu] rather
|
||||||
|
* than `ExposedDropdownMenuBox` — that component's read-only text field traps D-pad / controller
|
||||||
|
* focus (directional keys never leave it), so you can't navigate past it on a TV. Calls [onSelect]
|
||||||
|
* on a pick. A primary-colour border marks D-pad focus.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun <T> SettingDropdown(
|
private fun <T> SettingDropdown(
|
||||||
label: String,
|
label: String,
|
||||||
@@ -181,20 +188,35 @@ private fun <T> SettingDropdown(
|
|||||||
onSelect: (T) -> Unit,
|
onSelect: (T) -> Unit,
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var focused by remember { mutableStateOf(false) }
|
||||||
val selectedLabel = options.firstOrNull { it.first == selected }?.second
|
val selectedLabel = options.firstOrNull { it.first == selected }?.second
|
||||||
?: options.firstOrNull()?.second.orEmpty()
|
?: options.firstOrNull()?.second.orEmpty()
|
||||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
OutlinedTextField(
|
Surface(
|
||||||
value = selectedLabel,
|
onClick = { expanded = true },
|
||||||
onValueChange = {},
|
shape = MaterialTheme.shapes.small,
|
||||||
readOnly = true,
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
label = { Text(label) },
|
border = if (focused) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
.fillMaxWidth()
|
||||||
.fillMaxWidth(),
|
.onFocusChanged { focused = it.isFocused },
|
||||||
)
|
) {
|
||||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(selectedLabel, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
Icon(Icons.Filled.ArrowDropDown, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
options.forEach { (value, lbl) ->
|
options.forEach { (value, lbl) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(lbl) },
|
text = { Text(lbl) },
|
||||||
|
|||||||
Reference in New Issue
Block a user