Prepare for text2

Enabled physical keyboard support (#148), but not tested
This commit is contained in:
Sad Ellie 2024-02-05 23:30:20 +03:00
parent bf9bf37812
commit 5c81a1c675
8 changed files with 148 additions and 189 deletions

View File

@ -18,37 +18,46 @@
package com.sadellie.unitto.core.ui.common.textfield package com.sadellie.unitto.core.ui.common.textfield
import android.content.ClipData
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.base.Token
/** /**
* Copy value to clipboard with fractional symbols. * Unformatted values are expected. Basically what BigDecimal and Expression parser use.
* *
* Example: * @property formatterSymbols Current [FormatterSymbols].
* "123456.789" will be copied as "123456,789" * @property clipboardManager [android.content.ClipboardManager] provided by system.
*
* @param value Internal [TextFieldValue] without formatting with [Token.Digit.dot] as fractional.
*/ */
internal fun ClipboardManager.copyWithFractional( internal class ExpressionClipboardManager(
value: TextFieldValue, private val formatterSymbols: FormatterSymbols,
formatterSymbols: FormatterSymbols private val clipboardManager: android.content.ClipboardManager
) = this.setText( ): ClipboardManager {
AnnotatedString( override fun setText(annotatedString: AnnotatedString) = clipboardManager.setPrimaryClip(
value.annotatedString ClipData.newPlainText(
.subSequence(value.selection) PLAIN_TEXT_LABEL,
.text annotatedString
.replace(Token.Digit.dot, formatterSymbols.fractional) .text
.replace(Token.Digit.dot, formatterSymbols.fractional)
)
) )
)
internal fun ClipboardManager.copy(value: TextFieldValue) = this.setText( override fun getText(): AnnotatedString? = clipboardManager.primaryClip?.let { primaryClip ->
AnnotatedString( if (primaryClip.itemCount > 0) {
value.annotatedString val clipText = primaryClip.getItemAt(0)?.text ?:return@let null
.subSequence(value.selection)
.text clipText
) .toString()
) .toAnnotatedString()
} else {
null
}
}
override fun hasText() =
clipboardManager.primaryClipDescription?.hasMimeType("text/*") ?: false
}
internal const val PLAIN_TEXT_LABEL = "plain text" internal const val PLAIN_TEXT_LABEL = "plain text"
private fun CharSequence.toAnnotatedString(): AnnotatedString = AnnotatedString(this.toString())

View File

@ -18,7 +18,7 @@
package com.sadellie.unitto.core.ui.common.textfield package com.sadellie.unitto.core.ui.common.textfield
import androidx.compose.foundation.clickable import android.content.Context
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -32,27 +32,19 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalTextInputService import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import com.sadellie.unitto.core.ui.common.autosize.AutoSizeTextStyleBox import com.sadellie.unitto.core.ui.common.autosize.AutoSizeTextStyleBox
import com.sadellie.unitto.core.ui.common.textfield.texttoolbar.UnittoTextToolbar
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
@Composable @Composable
@ -60,105 +52,86 @@ fun ExpressionTextField(
modifier: Modifier, modifier: Modifier,
value: TextFieldValue, value: TextFieldValue,
minRatio: Float = 1f, minRatio: Float = 1f,
cutCallback: () -> Unit = {}, onValueChange: (TextFieldValue) -> Unit,
pasteCallback: (String) -> Unit = {},
onCursorChange: (TextRange) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
readOnly: Boolean = false, readOnly: Boolean = false,
placeholder: String = "", placeholder: String = "",
) { ) {
val localView = LocalView.current val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current val clipboardManager = remember(formatterSymbols) {
val expressionTransformer = remember(formatterSymbols) { ExpressionTransformer(formatterSymbols) } ExpressionClipboardManager(
formatterSymbols = formatterSymbols,
fun copyCallback() { clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE)
clipboardManager.copyWithFractional(value, formatterSymbols) as android.content.ClipboardManager
onCursorChange(TextRange(value.selection.end)) )
}
val expressionTransformer = remember(formatterSymbols) {
ExpressionTransformer(formatterSymbols)
} }
val textToolbar: UnittoTextToolbar = if (readOnly) { CompositionLocalProvider(
UnittoTextToolbar( LocalClipboardManager provides clipboardManager
view = localView, ) {
copyCallback = ::copyCallback, AutoSizeTextField(
) modifier = modifier,
} else { value = value,
UnittoTextToolbar( onValueChange = {
view = localView, onValueChange(it.copy(text = it.text.clearAndFilterExpression(formatterSymbols)))
copyCallback = ::copyCallback,
pasteCallback = {
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterExpression(formatterSymbols) ?: "")
}, },
cutCallback = { placeholder = placeholder,
clipboardManager.copyWithFractional(value, formatterSymbols) readOnly = readOnly,
cutCallback() textStyle = LocalNumberTypography.current.displayLarge.copy(textColor),
} visualTransformation = expressionTransformer,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
minRatio = minRatio
) )
} }
}
@Composable
fun NumberBaseTextField(
modifier: Modifier,
value: TextFieldValue,
minRatio: Float = 1f,
onValueChange: (TextFieldValue) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
readOnly: Boolean = false,
placeholder: String = "",
) {
AutoSizeTextField( AutoSizeTextField(
modifier = modifier, modifier = modifier,
value = value, value = value,
onValueChange = { onCursorChange(it.selection) }, onValueChange = {
onValueChange(it.copy(text = it.text.clearAndFilterNumberBase()))
},
placeholder = placeholder, placeholder = placeholder,
textToolbar = textToolbar,
readOnly = readOnly, readOnly = readOnly,
textStyle = LocalNumberTypography.current.displayLarge.copy(textColor), textStyle = LocalNumberTypography.current.displayLarge.copy(textColor),
visualTransformation = expressionTransformer,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
minRatio = minRatio minRatio = minRatio
) )
} }
@Composable @Composable
fun UnformattedTextField( fun SimpleTextField(
modifier: Modifier, modifier: Modifier,
value: TextFieldValue, value: TextFieldValue,
minRatio: Float = 1f, minRatio: Float = 1f,
cutCallback: () -> Unit = {}, onValueChange: (TextFieldValue) -> Unit,
pasteCallback: (String) -> Unit = {},
onCursorChange: (TextRange) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
readOnly: Boolean = false, readOnly: Boolean = false,
placeholder: String = "", placeholder: String = "",
) { ) {
val localView = LocalView.current
val clipboardManager = LocalClipboardManager.current
fun copyCallback() {
clipboardManager.copy(value)
onCursorChange(TextRange(value.selection.end))
}
val textToolbar: UnittoTextToolbar = remember(readOnly) {
if (readOnly) {
UnittoTextToolbar(
view = localView,
copyCallback = ::copyCallback,
)
} else {
UnittoTextToolbar(
view = localView,
copyCallback = ::copyCallback,
pasteCallback = {
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterNumberBase() ?: "")
},
cutCallback = {
clipboardManager.copy(value)
cutCallback()
}
)
}
}
AutoSizeTextField( AutoSizeTextField(
modifier = modifier, modifier = modifier,
value = value, value = value,
onValueChange = { onCursorChange(it.selection) }, onValueChange = onValueChange,
placeholder = placeholder, placeholder = placeholder,
textToolbar = textToolbar,
readOnly = readOnly, readOnly = readOnly,
textStyle = LocalNumberTypography.current.displayLarge.copy(color = textColor), textStyle = LocalNumberTypography.current.displayLarge.copy(textColor),
minRatio = minRatio, cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
minRatio = minRatio
) )
} }
@ -166,7 +139,6 @@ fun UnformattedTextField(
* Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300 * Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300
* *
* @param placeholder Placeholder text, shown when [value] is empty. * @param placeholder Placeholder text, shown when [value] is empty.
* @param textToolbar [TextToolbar] with modified actions in menu.
* @param alignment The alignment of the text within its container. * @param alignment The alignment of the text within its container.
* @see [BasicTextField] * @see [BasicTextField]
* @see [AutoSizeTextStyleBox] * @see [AutoSizeTextStyleBox]
@ -177,7 +149,6 @@ private fun AutoSizeTextField(
value: TextFieldValue, value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit, onValueChange: (TextFieldValue) -> Unit,
placeholder: String? = null, placeholder: String? = null,
textToolbar: TextToolbar = LocalTextToolbar.current,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
textStyle: TextStyle = TextStyle.Default, textStyle: TextStyle = TextStyle.Default,
@ -206,28 +177,14 @@ private fun AutoSizeTextField(
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalTextInputService provides null, LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) { ) {
val currentTextToolbar = LocalTextToolbar.current
val style = LocalTextStyle.current val style = LocalTextStyle.current
val focusRequester = remember { FocusRequester() }
BasicTextField( BasicTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(focusRequester)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
currentTextToolbar.hide()
focusRequester.requestFocus()
onValueChange(value.copy(selection = TextRange.Zero))
currentTextToolbar.showMenu(Rect(Offset.Zero, 0f))
}
),
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = style, textStyle = style,

View File

@ -46,11 +46,12 @@ class CalculatorScreenTest {
addTokens = {}, addTokens = {},
clearInput = {}, clearInput = {},
deleteTokens = {}, deleteTokens = {},
onCursorChange = {}, onValueChange = {},
toggleCalculatorMode = {}, toggleCalculatorMode = {},
equal = {}, equal = {},
clearHistory = {}, clearHistory = {},
addBracket = {} addBracket = {},
onDelete = {},
) )
} }
@ -69,7 +70,6 @@ class CalculatorScreenTest {
outputFormat = OutputFormat.PLAIN, outputFormat = OutputFormat.PLAIN,
formatterSymbols = FormatterSymbols.Spaces, formatterSymbols = FormatterSymbols.Spaces,
history = emptyList(), history = emptyList(),
allowVibration = false,
middleZero = false, middleZero = false,
acButton = true, acButton = true,
partialHistoryView = true partialHistoryView = true
@ -79,11 +79,12 @@ class CalculatorScreenTest {
addTokens = {}, addTokens = {},
clearInput = {}, clearInput = {},
deleteTokens = {}, deleteTokens = {},
onCursorChange = {}, onValueChange = {},
toggleCalculatorMode = {}, toggleCalculatorMode = {},
equal = {}, equal = {},
clearHistory = {}, clearHistory = {},
addBracket = {} addBracket = {},
onDelete = {},
) )
} }
@ -103,7 +104,6 @@ class CalculatorScreenTest {
outputFormat = OutputFormat.PLAIN, outputFormat = OutputFormat.PLAIN,
formatterSymbols = FormatterSymbols.Spaces, formatterSymbols = FormatterSymbols.Spaces,
history = emptyList(), history = emptyList(),
allowVibration = false,
middleZero = false, middleZero = false,
acButton = true, acButton = true,
partialHistoryView = true partialHistoryView = true
@ -113,11 +113,12 @@ class CalculatorScreenTest {
addTokens = {}, addTokens = {},
clearInput = {}, clearInput = {},
deleteTokens = {}, deleteTokens = {},
onCursorChange = {}, onValueChange = {},
toggleCalculatorMode = {}, toggleCalculatorMode = {},
equal = {}, equal = {},
clearHistory = {}, clearHistory = {},
addBracket = {} addBracket = {},
onDelete = {},
) )
} }

View File

@ -56,7 +56,6 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
@ -97,7 +96,7 @@ internal fun CalculatorRoute(
addTokens = viewModel::addTokens, addTokens = viewModel::addTokens,
clearInput = viewModel::clearInput, clearInput = viewModel::clearInput,
deleteTokens = viewModel::deleteTokens, deleteTokens = viewModel::deleteTokens,
onCursorChange = viewModel::onCursorChange, onValueChange = viewModel::updateInput,
toggleCalculatorMode = viewModel::updateRadianMode, toggleCalculatorMode = viewModel::updateRadianMode,
equal = viewModel::equal, equal = viewModel::equal,
clearHistory = viewModel::clearHistory, clearHistory = viewModel::clearHistory,
@ -115,7 +114,7 @@ internal fun CalculatorScreen(
addBracket: () -> Unit, addBracket: () -> Unit,
clearInput: () -> Unit, clearInput: () -> Unit,
deleteTokens: () -> Unit, deleteTokens: () -> Unit,
onCursorChange: (TextRange) -> Unit, onValueChange: (TextFieldValue) -> Unit,
toggleCalculatorMode: (Boolean) -> Unit, toggleCalculatorMode: (Boolean) -> Unit,
equal: () -> Unit, equal: () -> Unit,
clearHistory: () -> Unit, clearHistory: () -> Unit,
@ -130,7 +129,7 @@ internal fun CalculatorScreen(
addSymbol = addTokens, addSymbol = addTokens,
clearSymbols = clearInput, clearSymbols = clearInput,
deleteSymbol = deleteTokens, deleteSymbol = deleteTokens,
onCursorChange = onCursorChange, onValueChange = onValueChange,
toggleAngleMode = { toggleCalculatorMode(!uiState.radianMode) }, toggleAngleMode = { toggleCalculatorMode(!uiState.radianMode) },
equal = equal, equal = equal,
clearHistory = clearHistory, clearHistory = clearHistory,
@ -149,7 +148,7 @@ private fun Ready(
addBracket: () -> Unit, addBracket: () -> Unit,
clearSymbols: () -> Unit, clearSymbols: () -> Unit,
deleteSymbol: () -> Unit, deleteSymbol: () -> Unit,
onCursorChange: (TextRange) -> Unit, onValueChange: (TextFieldValue) -> Unit,
toggleAngleMode: () -> Unit, toggleAngleMode: () -> Unit,
equal: () -> Unit, equal: () -> Unit,
clearHistory: () -> Unit, clearHistory: () -> Unit,
@ -266,9 +265,7 @@ private fun Ready(
), ),
formatterSymbols = uiState.formatterSymbols, formatterSymbols = uiState.formatterSymbols,
input = uiState.input, input = uiState.input,
deleteSymbol = deleteSymbol, onValueChange = onValueChange,
addSymbol = addSymbol,
onCursorChange = onCursorChange,
output = uiState.output output = uiState.output
) )
@ -379,7 +376,7 @@ private fun PreviewCalculatorScreen() {
addTokens = {}, addTokens = {},
clearInput = {}, clearInput = {},
deleteTokens = {}, deleteTokens = {},
onCursorChange = {}, onValueChange = {},
toggleCalculatorMode = {}, toggleCalculatorMode = {},
equal = {}, equal = {},
clearHistory = {}, clearHistory = {},

View File

@ -166,11 +166,11 @@ internal class CalculatorViewModel @Inject constructor(
TextFieldValue() TextFieldValue()
} }
fun onCursorChange(selection: TextRange) = _input.update { fun updateInput(value: TextFieldValue) = _input.update {
// Without this line: will place token (even in the middle of the input) and place cursor at // Without this line: will place token (even in the middle of the input) and place cursor at
// the end. This line also removes fractional output once user touches input text field // the end. This line also removes fractional output once user touches input text field
_equalClicked.update { false } _equalClicked.update { false }
it.copy(selection = selection) value
} }
fun updateRadianMode(newValue: Boolean) = viewModelScope.launch { fun updateRadianMode(newValue: Boolean) = viewModelScope.launch {

View File

@ -37,14 +37,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.LocalWindowSize import com.sadellie.unitto.core.ui.LocalWindowSize
import com.sadellie.unitto.core.ui.WindowHeightSizeClass import com.sadellie.unitto.core.ui.WindowHeightSizeClass
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField import com.sadellie.unitto.core.ui.common.textfield.SimpleTextField
import com.sadellie.unitto.feature.calculator.CalculationResult import com.sadellie.unitto.feature.calculator.CalculationResult
@Composable @Composable
@ -52,9 +51,7 @@ fun TextBox(
modifier: Modifier, modifier: Modifier,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
input: TextFieldValue, input: TextFieldValue,
deleteSymbol: () -> Unit, onValueChange: (TextFieldValue) -> Unit,
addSymbol: (String) -> Unit,
onCursorChange: (TextRange) -> Unit,
output: CalculationResult, output: CalculationResult,
) { ) {
Column( Column(
@ -77,9 +74,7 @@ fun TextBox(
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = input, value = input,
minRatio = 0.5f, minRatio = 0.5f,
cutCallback = deleteSymbol, onValueChange = onValueChange,
pasteCallback = addSymbol,
onCursorChange = onCursorChange,
formatterSymbols = formatterSymbols formatterSymbols = formatterSymbols
) )
if (LocalWindowSize.current.heightSizeClass > WindowHeightSizeClass.Compact) { if (LocalWindowSize.current.heightSizeClass > WindowHeightSizeClass.Compact) {
@ -105,9 +100,9 @@ fun TextBox(
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = outputTF, value = outputTF,
minRatio = 0.8f, minRatio = 0.8f,
onCursorChange = { outputTF = outputTF.copy(selection = it) }, onValueChange = { outputTF = it },
formatterSymbols = formatterSymbols,
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f), textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
formatterSymbols = formatterSymbols,
readOnly = true, readOnly = true,
) )
} }
@ -123,36 +118,36 @@ fun TextBox(
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = outputTF, value = outputTF,
minRatio = 0.8f, minRatio = 0.8f,
onCursorChange = { outputTF = outputTF.copy(selection = it) }, onValueChange = { outputTF = it },
formatterSymbols = formatterSymbols,
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f), textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
formatterSymbols = formatterSymbols,
readOnly = true, readOnly = true,
) )
} }
is CalculationResult.DivideByZeroError -> { is CalculationResult.DivideByZeroError -> {
UnformattedTextField( SimpleTextField(
modifier = Modifier modifier = Modifier
.weight(2f) .weight(2f)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = TextFieldValue(stringResource(output.label)), value = TextFieldValue(stringResource(output.label)),
minRatio = 0.8f, minRatio = 0.8f,
onCursorChange = {}, onValueChange = {},
textColor = MaterialTheme.colorScheme.error, textColor = MaterialTheme.colorScheme.error,
readOnly = true, readOnly = true,
) )
} }
is CalculationResult.Error -> { is CalculationResult.Error -> {
UnformattedTextField( SimpleTextField(
modifier = Modifier modifier = Modifier
.weight(2f) .weight(2f)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = TextFieldValue(stringResource(output.label)), value = TextFieldValue(stringResource(output.label)),
minRatio = 0.8f, minRatio = 0.8f,
onCursorChange = {}, onValueChange = {},
textColor = MaterialTheme.colorScheme.error, textColor = MaterialTheme.colorScheme.error,
readOnly = true, readOnly = true,
) )

View File

@ -64,7 +64,6 @@ import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -83,7 +82,8 @@ import com.sadellie.unitto.core.ui.common.ScaffoldWithTopBar
import com.sadellie.unitto.core.ui.common.SettingsButton import com.sadellie.unitto.core.ui.common.SettingsButton
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField import com.sadellie.unitto.core.ui.common.textfield.NumberBaseTextField
import com.sadellie.unitto.core.ui.common.textfield.SimpleTextField
import com.sadellie.unitto.core.ui.datetime.formatDateWeekDayMonthYear import com.sadellie.unitto.core.ui.datetime.formatDateWeekDayMonthYear
import com.sadellie.unitto.data.common.format import com.sadellie.unitto.data.common.format
import com.sadellie.unitto.data.converter.UnitID import com.sadellie.unitto.data.converter.UnitID
@ -114,7 +114,7 @@ internal fun ConverterRoute(
processInput = viewModel::addTokens, processInput = viewModel::addTokens,
deleteDigit = viewModel::deleteTokens, deleteDigit = viewModel::deleteTokens,
clearInput = viewModel::clearInput, clearInput = viewModel::clearInput,
onCursorChange = viewModel::onCursorChange, onValueChange = viewModel::updateInput,
onFocusOnInput2 = viewModel::updateFocused, onFocusOnInput2 = viewModel::updateFocused,
onErrorClick = viewModel::updateCurrencyRates, onErrorClick = viewModel::updateCurrencyRates,
addBracket = viewModel::addBracket addBracket = viewModel::addBracket
@ -132,7 +132,7 @@ private fun ConverterScreen(
processInput: (String) -> Unit, processInput: (String) -> Unit,
deleteDigit: () -> Unit, deleteDigit: () -> Unit,
clearInput: () -> Unit, clearInput: () -> Unit,
onCursorChange: (TextRange) -> Unit, onValueChange: (TextFieldValue) -> Unit,
onFocusOnInput2: (Boolean) -> Unit, onFocusOnInput2: (Boolean) -> Unit,
onErrorClick: (AbstractUnit) -> Unit, onErrorClick: (AbstractUnit) -> Unit,
addBracket: () -> Unit, addBracket: () -> Unit,
@ -148,7 +148,7 @@ private fun ConverterScreen(
NumberBase( NumberBase(
modifier = Modifier.padding(it), modifier = Modifier.padding(it),
uiState = uiState, uiState = uiState,
onCursorChange = onCursorChange, onValueChange = onValueChange,
processInput = processInput, processInput = processInput,
deleteDigit = deleteDigit, deleteDigit = deleteDigit,
navigateToLeftScreen = navigateToLeftScreen, navigateToLeftScreen = navigateToLeftScreen,
@ -167,7 +167,7 @@ private fun ConverterScreen(
Default( Default(
modifier = Modifier.padding(it), modifier = Modifier.padding(it),
uiState = uiState, uiState = uiState,
onCursorChange = onCursorChange, onValueChange = onValueChange,
onFocusOnInput2 = onFocusOnInput2, onFocusOnInput2 = onFocusOnInput2,
processInput = processInput, processInput = processInput,
deleteDigit = deleteDigit, deleteDigit = deleteDigit,
@ -204,7 +204,7 @@ private fun UnitConverterTopBar(
private fun NumberBase( private fun NumberBase(
modifier: Modifier, modifier: Modifier,
uiState: UnitConverterUIState.NumberBase, uiState: UnitConverterUIState.NumberBase,
onCursorChange: (TextRange) -> Unit, onValueChange: (TextFieldValue) -> Unit,
processInput: (String) -> Unit, processInput: (String) -> Unit,
deleteDigit: () -> Unit, deleteDigit: () -> Unit,
navigateToLeftScreen: () -> Unit, navigateToLeftScreen: () -> Unit,
@ -218,14 +218,12 @@ private fun NumberBase(
ColumnWithConstraints(modifier = contentModifier) { ColumnWithConstraints(modifier = contentModifier) {
val textFieldModifier = Modifier.weight(2f) val textFieldModifier = Modifier.weight(2f)
UnformattedTextField( NumberBaseTextField(
modifier = textFieldModifier, modifier = textFieldModifier,
minRatio = 0.7f, minRatio = 0.7f,
placeholder = Token.Digit._0, placeholder = Token.Digit._0,
value = uiState.input, value = uiState.input,
onCursorChange = onCursorChange, onValueChange = onValueChange,
pasteCallback = processInput,
cutCallback = deleteDigit,
) )
AnimatedUnitShortName(stringResource(uiState.unitFrom.shortName)) AnimatedUnitShortName(stringResource(uiState.unitFrom.shortName))
@ -261,7 +259,7 @@ private fun NumberBase(
private fun Default( private fun Default(
modifier: Modifier, modifier: Modifier,
uiState: UnitConverterUIState.Default, uiState: UnitConverterUIState.Default,
onCursorChange: (TextRange) -> Unit, onValueChange: (TextFieldValue) -> Unit,
onFocusOnInput2: (Boolean) -> Unit, onFocusOnInput2: (Boolean) -> Unit,
processInput: (String) -> Unit, processInput: (String) -> Unit,
deleteDigit: () -> Unit, deleteDigit: () -> Unit,
@ -330,13 +328,11 @@ private fun Default(
) { ) {
ExpressionTextField( ExpressionTextField(
modifier = Modifier.fillMaxWidth().weight(1f), modifier = Modifier.fillMaxWidth().weight(1f),
minRatio = 0.7f,
placeholder = Token.Digit._0,
value = uiState.input1, value = uiState.input1,
onCursorChange = onCursorChange, minRatio = 0.7f,
pasteCallback = processInput, onValueChange = onValueChange,
cutCallback = deleteDigit,
formatterSymbols = uiState.formatterSymbols, formatterSymbols = uiState.formatterSymbols,
placeholder = Token.Digit._0,
) )
AnimatedUnitShortName(stringResource(uiState.unitFrom.shortName)) AnimatedUnitShortName(stringResource(uiState.unitFrom.shortName))
} }
@ -351,13 +347,11 @@ private fun Default(
ExpressionTextField( ExpressionTextField(
modifier = Modifier.fillMaxWidth().weight(1f) modifier = Modifier.fillMaxWidth().weight(1f)
.onFocusEvent { state -> onFocusOnInput2(state.hasFocus) }, .onFocusEvent { state -> onFocusOnInput2(state.hasFocus) },
minRatio = 0.7f,
placeholder = Token.Digit._0,
value = uiState.input2, value = uiState.input2,
onCursorChange = onCursorChange, minRatio = 0.7f,
pasteCallback = processInput, onValueChange = onValueChange,
cutCallback = deleteDigit,
formatterSymbols = uiState.formatterSymbols, formatterSymbols = uiState.formatterSymbols,
placeholder = Token.Digit._0,
) )
AnimatedUnitShortName(stringResource(R.string.unit_inch_short)) AnimatedUnitShortName(stringResource(R.string.unit_inch_short))
} }
@ -365,13 +359,11 @@ private fun Default(
} else { } else {
ExpressionTextField( ExpressionTextField(
modifier = textFieldModifier, modifier = textFieldModifier,
minRatio = 0.7f,
placeholder = Token.Digit._0,
value = uiState.input1, value = uiState.input1,
onCursorChange = onCursorChange, minRatio = 0.7f,
pasteCallback = processInput, onValueChange = onValueChange,
cutCallback = deleteDigit,
formatterSymbols = uiState.formatterSymbols, formatterSymbols = uiState.formatterSymbols,
placeholder = Token.Digit._0,
) )
AnimatedVisibility( AnimatedVisibility(
visible = calculation.text.isNotEmpty(), visible = calculation.text.isNotEmpty(),
@ -382,10 +374,10 @@ private fun Default(
ExpressionTextField( ExpressionTextField(
modifier = Modifier, modifier = Modifier,
value = calculation, value = calculation,
onCursorChange = { calculation = calculation.copy(selection = it) },
formatterSymbols = uiState.formatterSymbols,
minRatio = 0.7f, minRatio = 0.7f,
onValueChange = { calculation = it },
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
formatterSymbols = uiState.formatterSymbols,
readOnly = true readOnly = true
) )
} }
@ -456,20 +448,20 @@ private fun ConverterResultTextField(
when (result) { when (result) {
is ConverterResult.Loading -> { is ConverterResult.Loading -> {
UnformattedTextField( SimpleTextField(
modifier = modifier, modifier = modifier,
value = TextFieldValue(stringResource(R.string.loading_label)), value = TextFieldValue(stringResource(R.string.loading_label)),
onCursorChange = {}, onValueChange = {},
minRatio = 0.7f, minRatio = 0.7f,
readOnly = true readOnly = true
) )
} }
is ConverterResult.Error -> { is ConverterResult.Error -> {
UnformattedTextField( SimpleTextField(
modifier = modifier, modifier = modifier,
value = TextFieldValue(stringResource(R.string.error_label)), value = TextFieldValue(stringResource(R.string.error_label)),
onCursorChange = { onErrorClick() }, onValueChange = { onErrorClick() },
minRatio = 0.7f, minRatio = 0.7f,
readOnly = true, readOnly = true,
textColor = MaterialTheme.colorScheme.error, textColor = MaterialTheme.colorScheme.error,
@ -480,20 +472,29 @@ private fun ConverterResultTextField(
ExpressionTextField( ExpressionTextField(
modifier = modifier, modifier = modifier,
value = resultTextField, value = resultTextField,
onCursorChange = { resultTextField = resultTextField.copy(selection = it) }, minRatio = 0.7f,
onValueChange = { resultTextField = it },
formatterSymbols = formatterSymbols, formatterSymbols = formatterSymbols,
readOnly = true
)
}
is ConverterResult.NumberBase -> {
NumberBaseTextField(
modifier = modifier,
value = resultTextField,
onValueChange = { resultTextField = it },
minRatio = 0.7f, minRatio = 0.7f,
readOnly = true readOnly = true
) )
} }
is ConverterResult.NumberBase,
is ConverterResult.Time, is ConverterResult.Time,
is ConverterResult.FootInch -> { is ConverterResult.FootInch -> {
UnformattedTextField( SimpleTextField(
modifier = modifier, modifier = modifier,
value = resultTextField, value = resultTextField,
onCursorChange = { resultTextField = resultTextField.copy(selection = it) }, onValueChange = { resultTextField = it },
minRatio = 0.7f, minRatio = 0.7f,
readOnly = true readOnly = true
) )
@ -585,7 +586,7 @@ private fun PreviewConverterScreen() {
processInput = {}, processInput = {},
deleteDigit = {}, deleteDigit = {},
clearInput = {}, clearInput = {},
onCursorChange = {}, onValueChange = {},
onFocusOnInput2 = {}, onFocusOnInput2 = {},
onErrorClick = {}, onErrorClick = {},
addBracket = {} addBracket = {}

View File

@ -18,7 +18,6 @@
package com.sadellie.unitto.feature.converter package com.sadellie.unitto.feature.converter
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -321,7 +320,7 @@ internal class ConverterViewModel @Inject constructor(
} }
} }
fun onCursorChange(selection: TextRange) = _input1.update { it.copy(selection = selection) } fun updateInput(value: TextFieldValue) = _input1.update { value }
fun updateCurrencyRates(unit: AbstractUnit) { fun updateCurrencyRates(unit: AbstractUnit) {
_loadCurrenciesJob = viewModelScope.launch(Dispatchers.IO) { _loadCurrenciesJob = viewModelScope.launch(Dispatchers.IO) {