From e5b9879d108d5b76dc27a8f1aadf321166146c96 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Tue, 2 Jun 2026 18:40:26 -0400 Subject: [PATCH] Minor fixes to Compose UI - Adjust window insets to allow scrolling from inset region - Use TextFieldState for all TextFields - Make TextFields single line where appropriate - Remove viewbinding generation now that we don't use views - Use rememberUpdatedState where appropriate - Remove @Composable for functions that don't need it Signed-off-by: Andrew Gunnerson --- app/build.gradle.kts | 1 - .../bcr/rule/RecordRuleEditorScreen.kt | 4 +- .../bcr/settings/FileRetentionDialog.kt | 32 ++++++++------- .../bcr/settings/FilenameTemplateDialog.kt | 40 +++++++++---------- .../bcr/settings/FormatParamDialog.kt | 22 +++++----- .../bcr/settings/FormatSampleRateDialog.kt | 22 +++++----- .../bcr/settings/MinDurationDialog.kt | 22 +++++----- .../main/java/com/chiller3/bcr/ui/Screen.kt | 4 +- 8 files changed, 81 insertions(+), 66 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d58f2c05e..a1278ed15 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -167,7 +167,6 @@ android { buildConfig = true compose = true resValues = true - viewBinding = true } lint { // The translations are always going to lag behind new strings being diff --git a/app/src/main/java/com/chiller3/bcr/rule/RecordRuleEditorScreen.kt b/app/src/main/java/com/chiller3/bcr/rule/RecordRuleEditorScreen.kt index 75ad4c863..071386cd8 100644 --- a/app/src/main/java/com/chiller3/bcr/rule/RecordRuleEditorScreen.kt +++ b/app/src/main/java/com/chiller3/bcr/rule/RecordRuleEditorScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -61,8 +62,9 @@ fun RecordRuleEditorScreen( val resources = LocalResources.current val rule by viewModel.initOrGetExisting(initialRule).collectAsStateWithLifecycle() + val latestOnRuleUpdate by rememberUpdatedState(onRuleUpdate) LaunchedEffect(rule) { - onRuleUpdate(rule) + latestOnRuleUpdate(rule) } val contactInfo by viewModel.contactInfo.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/chiller3/bcr/settings/FileRetentionDialog.kt b/app/src/main/java/com/chiller3/bcr/settings/FileRetentionDialog.kt index bb92bcf93..4920a2e33 100644 --- a/app/src/main/java/com/chiller3/bcr/settings/FileRetentionDialog.kt +++ b/app/src/main/java/com/chiller3/bcr/settings/FileRetentionDialog.kt @@ -10,17 +10,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.then import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -41,15 +41,14 @@ fun FileRetentionDialog( ) { val resources = LocalResources.current - var input by rememberSaveable(retention) { - mutableStateOf( - when (retention) { - is DaysRetention -> retention.days.toString() - NoRetention -> "" - } - ) + val initialText = remember { + when (retention) { + is DaysRetention -> retention.days.toString() + NoRetention -> "" + } } - val retention = tryParseInput(input) + val input = rememberTextFieldState(initialText = initialText) + val retention = tryParseInput(input.text.toString()) val supportingText = remember(retention) { when (retention) { is RetentionParse.Value -> retention.retention.toFormattedString(resources) @@ -64,12 +63,17 @@ fun FileRetentionDialog( Text(text = stringResource(R.string.file_retention_dialog_message)) OutlinedTextField( + state = input, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - value = input, - onValueChange = { if (it.isDigitsOnly()) input = it }, isError = retention is RetentionParse.Error, supportingText = { Text(text = supportingText) }, + inputTransformation = InputTransformation.then { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, ) } }, diff --git a/app/src/main/java/com/chiller3/bcr/settings/FilenameTemplateDialog.kt b/app/src/main/java/com/chiller3/bcr/settings/FilenameTemplateDialog.kt index 33d330d77..1f8069d03 100644 --- a/app/src/main/java/com/chiller3/bcr/settings/FilenameTemplateDialog.kt +++ b/app/src/main/java/com/chiller3/bcr/settings/FilenameTemplateDialog.kt @@ -13,24 +13,21 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.chiller3.bcr.BuildConfig @@ -48,17 +45,9 @@ fun FilenameTemplateDialog( onReset: () -> Unit, ) { val syntaxColors = templateSyntaxColors() - // This intentionally does not key off anything. We don't want to reset the text box state, only - // rehighlight the template if needed. - var input by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(syntaxColors.annotated(template.toString()))) - } - val template = tryParseInput(input.text, syntaxColors) - - LaunchedEffect(syntaxColors) { - val orig = input - input = orig.copy(annotatedString = syntaxColors.annotated(orig.text)) - } + val initialText = remember { template.toString() } + val input = rememberTextFieldState(initialText = initialText) + val template = tryParseInput(input.text.toString(), syntaxColors) AlertDialog( title = { Text(text = stringResource(R.string.filename_template_dialog_title)) }, @@ -67,17 +56,26 @@ fun FilenameTemplateDialog( Text(text = buildMessage(syntaxColors)) OutlinedTextField( + state = input, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - value = input, - onValueChange = { - input = it.copy(annotatedString = syntaxColors.annotated(it.text)) - }, isError = template is TemplateParse.Error, supportingText = { if (template is TemplateParse.Error) { Text(text = template.message) } }, + outputTransformation = OutputTransformation { + // This can't use an AnnotatedString directly. + // https://issuetracker.google.com/issues/389978748 + val annotated = syntaxColors.annotated(originalText.toString()) + + for (style in annotated.spanStyles) { + addStyle(style.item, style.start, style.end) + } + for (style in annotated.paragraphStyles) { + addStyle(style.item, style.start, style.end) + } + }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false, diff --git a/app/src/main/java/com/chiller3/bcr/settings/FormatParamDialog.kt b/app/src/main/java/com/chiller3/bcr/settings/FormatParamDialog.kt index c2cc74422..b99a19ef0 100644 --- a/app/src/main/java/com/chiller3/bcr/settings/FormatParamDialog.kt +++ b/app/src/main/java/com/chiller3/bcr/settings/FormatParamDialog.kt @@ -10,6 +10,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.then import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.LocalTextStyle @@ -17,11 +21,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -61,8 +61,8 @@ fun FormatParamDialog( } } - var input by rememberSaveable { mutableStateOf("") } - val value = tryParseInput(paramInfo, multiplier, input) + val input = rememberTextFieldState() + val value = tryParseInput(paramInfo, multiplier, input.text.toString()) val settings = formatParamTextFieldSettings(paramInfo, multiplier) AlertDialog( @@ -77,9 +77,8 @@ fun FormatParamDialog( Text(text = message) OutlinedTextField( + state = input, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - value = input, - onValueChange = { if (it.isDigitsOnly()) input = it }, textStyle = LocalTextStyle.current.copy(textAlign = settings.textAlign), prefix = { if (settings.prefix != null) { @@ -91,7 +90,13 @@ fun FormatParamDialog( Text(text = settings.suffix) } }, + inputTransformation = InputTransformation.then { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, ) } }, @@ -116,7 +121,6 @@ fun FormatParamDialog( ) } -@Composable private fun tryParseInput(paramInfo: RangedParamInfo, multiplier: UInt, input: String): UInt? { if (input.isNotEmpty()) { try { diff --git a/app/src/main/java/com/chiller3/bcr/settings/FormatSampleRateDialog.kt b/app/src/main/java/com/chiller3/bcr/settings/FormatSampleRateDialog.kt index 91c957e14..f1e9bfc7b 100644 --- a/app/src/main/java/com/chiller3/bcr/settings/FormatSampleRateDialog.kt +++ b/app/src/main/java/com/chiller3/bcr/settings/FormatSampleRateDialog.kt @@ -10,6 +10,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.then import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.LocalTextStyle @@ -17,10 +21,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -46,8 +46,8 @@ fun FormatSampleRateDialog( throw IllegalStateException("Selected format is not configurable") } - var input by rememberSaveable { mutableStateOf("") } - val value = tryParseInput(sampleRateInfo, input) + val input = rememberTextFieldState() + val value = tryParseInput(sampleRateInfo, input.text.toString()) val settings = formatSampleRateTextFieldSettings() AlertDialog( @@ -57,9 +57,8 @@ fun FormatSampleRateDialog( Text(text = sampleRateMessage(sampleRateInfo)) OutlinedTextField( + state = input, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - value = input, - onValueChange = { if (it.isDigitsOnly()) input = it }, textStyle = LocalTextStyle.current.copy(textAlign = settings.textAlign), prefix = { if (settings.prefix != null) { @@ -71,7 +70,13 @@ fun FormatSampleRateDialog( Text(text = settings.suffix) } }, + inputTransformation = InputTransformation.then { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, ) } }, @@ -115,7 +120,6 @@ private fun sampleRateMessage(sampleRateInfo: RangedSampleRateInfo) = buildAnnot } } -@Composable private fun tryParseInput(sampleRateInfo: SampleRateInfo, input: String): UInt? { if (input.isNotEmpty()) { try { diff --git a/app/src/main/java/com/chiller3/bcr/settings/MinDurationDialog.kt b/app/src/main/java/com/chiller3/bcr/settings/MinDurationDialog.kt index a289d7789..4ed2f750f 100644 --- a/app/src/main/java/com/chiller3/bcr/settings/MinDurationDialog.kt +++ b/app/src/main/java/com/chiller3/bcr/settings/MinDurationDialog.kt @@ -10,16 +10,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.then import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -35,8 +35,8 @@ fun MinDurationDialog( onSelect: (Int) -> Unit, onDismiss: () -> Unit, ) { - var input by rememberSaveable(minDuration) { mutableStateOf(minDuration.toString()) } - val minDuration = tryParseInput(input) + val input = rememberTextFieldState(initialText = minDuration.toString()) + val minDuration = tryParseInput(input.text.toString()) val supportingText = minDuration?.let { pluralStringResource(R.plurals.min_duration_dialog_seconds, it, it) } @@ -48,11 +48,16 @@ fun MinDurationDialog( Text(text = stringResource(R.string.min_duration_dialog_message)) OutlinedTextField( + state = input, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - value = input, - onValueChange = { if (it.isDigitsOnly()) input = it }, supportingText = { supportingText?.let { Text(text = it) } }, + inputTransformation = InputTransformation.then { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, ) } }, @@ -77,7 +82,6 @@ fun MinDurationDialog( ) } -@Composable private fun tryParseInput(input: String): Int? { if (input.isEmpty()) { return 0 diff --git a/app/src/main/java/com/chiller3/bcr/ui/Screen.kt b/app/src/main/java/com/chiller3/bcr/ui/Screen.kt index cc814b71a..7b0ae5c51 100644 --- a/app/src/main/java/com/chiller3/bcr/ui/Screen.kt +++ b/app/src/main/java/com/chiller3/bcr/ui/Screen.kt @@ -110,8 +110,8 @@ fun AppScreen( }, containerColor = PreferenceDefaults.containerColor, ) { contentPadding -> - val outerPadding = contentPadding.copy(bottom = 0.dp) - val innerPadding = contentPadding.copy(start = 0.dp, top = 0.dp, end = 0.dp) + val outerPadding = contentPadding.copy(start = 0.dp, end = 0.dp, bottom = 0.dp) + val innerPadding = contentPadding.copy(top = 0.dp) Box(modifier = Modifier.padding(outerPadding)) { content(AppScreenParams(