How to handle key events during TextField editing in Compose? - kotlin

I'm making a chess engine on desktop compose, one of the things I'm trying to implement is a TextField where i can paste in several moves to recreate games.
I'm having problems saving the text that I input into my TextField composable.
My text composable is as follows, I understand that with my current implementation it prints every time the move variable changes, but i just wanted this to happen when I press the ENTER key on my keyboard.
I'm using the print to try some code but what i want to do is to save the String to a list or something, but that i can implement in my own later.
I only find explanations on how to do it with android and android specific methods.
Text(" Play", textAlign = TextAlign.Center, fontSize = 30.sp)
val move = remember { mutableStateOf("Play") }
TextField(
value = move.value,
onValueChange = { move.value = it },
label = { Text("Move") },
maxLines = 1,
textStyle = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
)
println(move.value)
It has been pointed out to me a solution to handle the ENTER keybind as been answered here:
How to trigger PC Keyboard inputs in Kotlin Desktop Compose
I'm just having some issues, that I press ENTER unable to handle the String when I press ENTER it prints continuosly isntead of only once based on my research I need to implement something like this:
Box doesn't capture key events in Compose Desktop
but i can't seem to be able to use the KeyEvent.ACTION_UP not sure if it's specific to the editText, if I'm missing some imports, or if there's another way to do it with the TextField composable.
My code after the given suggestions
val requester = remember { FocusRequester() }
LaunchedEffect(Unit) {
requester.requestFocus()
}
Text(" Play", textAlign = TextAlign.Center, fontSize = 30.sp)
val move = remember { mutableStateOf("Play") }
TextField(
value = move.value,
onValueChange = { move.value = it },
label = { Text("Move") },
maxLines = 1,
textStyle = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
.onKeyEvent {
//if(keyCode==KeyEvent.KEYCODE_ENTER&&event.action==KeyEvent.ACTION_UP){
if (it.key == Key.Enter) {
println(move.value)
true
} else {
// let other handlers receive this event
false
}
}
.focusRequester(requester)
.focusable()
)
I managed to fix this problem by changing the if condition to this:
if (it.key == Key.Enter && move.value!="") {
println(move.value)
move.value = ""
true
}
So that everytime I write something and press ENTER it prints the String and clears the mutableState, and while ENTER is still pressed it won't print because the mutableState is an empty String.
I'm still looking for a better solution than this

You can capture key events with Modifier.onKeyEvent. With the text field you don't need any focus capturing, because it's already there. If you would need to do the same for a custom view, check out this answer
To check which button was pressed you can use key, and to check where it was released, you can check type:
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.onKeyEvent { keyEvent ->
if (keyEvent.key != Key.Enter) return#onKeyEvent false
if (keyEvent.type == KeyEventType.KeyUp) {
println("Enter released")
}
true
}
)

Related

Element after LazyColumn (Keep scrolling LazyColumn)

Might be pretty basic but I can't figure it out. So my goal is to display some text after the last list item. But seems like scrolling down the LazyColumn just gets me to the last LazyColumn item therefore the text that goes afterwards is not visible.
I want to be able to keep scrolling
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn() {
items(list) {
//display list elements
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(text = "end")
Example Image
You can probably use item to achieve this:
LazyColumn() {
items(list) {
//display list elements
}
item {
Spacer(modifier = Modifier.height(8.dp))
Text(text = "end")
}
}

Why won't my checkbox UI update on the first click, but will update on every click after that?

I have a Jetpack Compose for Desktop UI application that shows a popup with a list of enums, each of which has a checkbox to toggle them:
Sorry for the long code, this is as small a MCVE as I could make of it.
enum class MyEnum {
A, B, C
}
data class MyFilter(val enums: Collection<MyEnum>)
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun App() {
var filter by remember { mutableStateOf(MyFilter(emptyList())) }
MaterialTheme {
var show by remember { mutableStateOf(false) }
if (show) {
val selected = remember { filter.enums.toMutableStateList() }
AlertDialog({ show = false }, text = {
Column {
MyEnum.values().forEach { e ->
Row {
val isSelected by remember { derivedStateOf { e in selected } }
Checkbox(isSelected, { if (isSelected) selected.remove(e) else selected.add(e) })
Text(e.name)
}
}
}
}, buttons = {
Button({
filter = MyFilter(selected.toList())
show = false
}) { Text("OK") }
})
}
Button({ show = true }) { Text("Open") }
}
}
The problem here is, the very first time after opening the dialog, a click on a checkbox will properly update the underlying selected list, but it won't update the UI. So the checkbox doesn't appear to change state. Every click after that will properly update the UI, including the changed state for the previous click.
This happens reliably, every single time the dialog is opened, but only on the first checkbox click.
Why does this happen, and how can I fix it?

Kotlin Jetpack Compose how to pass any icon and its color from outside

How could i pass any icon and icon color from outside? In React Native i would use icon?: React.ReactNode as a prop, but how to do this in Kotlin? Is there a type that allows to pass icon from outside? The code that i have below does not work, but it represents the idea that i want to achieve.
#Composable
fun IconField(
label: String,
icon: Boolean? = false, //In React native i would do this like icon?: React.ReactNode
) {
Row() {
if(icon)
Icon(
contentDescription = "",
modifier = Modifier
.size(16.dp))
Text(
text = (label)
)
}
}
From outside would want to pass tint and icon from outsie so that i could easily change icons and colors
IconField(label = "Icon Field", icon = Icons.Default.Check, tint = Color.Black, icon = true )
For your particular case you can simply pass icon as nullable argument
#Composable
fun IconField(
label: String,
modifier: Modifier = Modifier,
leadingIcon: ImageVector? = null // You can change ImageVector with Painter or ImageBitmap
iconTint: Color = MaterialTheme.colors.Black
) {
Row(modifier = modifier) {
leadingIcon?.let {
Icon(
modifier = Modifier.size(16.dp),
imageVector = leadingIcon,
contentDescription = null,
tint = iconTint
)
} ?: Box(Modifier.size(16.dp)) // You can set dummy box for save Text positioning
Text(text = label)
}
}

Remember in Composition is forgotten

I'm at Google CodeLabs:
And here is the code:
#Composable
fun WaterCounter(
modifier: Modifier = Modifier,
) {
var count by remember { mutableStateOf(0) }
Column(
modifier = modifier.padding(16.dp)
) {
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
taskName = "Have you taken your 15 minute walk today?",
onClose = { showTask = false })
}
Text(
text = "You've had $count glasses",
modifier = modifier.padding(16.dp)
)
}
Row(
modifier = modifier.padding(top = 8.dp)
) {
Button(
onClick = { count++ },
enabled = count < 10,
) {
Text(text = "add one")
}
Button(
onClick = { count = 0 },
modifier = modifier.padding(start = 8.dp)
) {
Text(text = "Clear water count")
}
}
}
}
I can't get this phrase:
"Press the Clear water count button to reset count to 0 and cause a recomposition. Text showing count, and all code related to WellnessTaskItem, are not invoked and leave the Composition. showTask is forgotten because the code location where remember showTask is called was not invoked."
Please help me understand why show task is forgotten? What does it mean "the code was not invoked" and "it leaves the composition"?
Conditional code blocks enter composition when condition is true and leave composition when condition cease to be true. When count is zero that block is removed and next time it enters composition remember stores showTask with true value.
For instance the code block below
#Composable fun App() {
val result = getData()
if (result == null) {
Loading(...)
} else {
Header(result)
Body(result)
}
}
these blocks also enter composition based on result is null or not.
Entering Composition means it's a node that starts execution and when a State that is read in this block is updated recomposition happens which means same block is updated with new value.
when result is null Loading Composable enters composition, let's say it has a progress bar with value when it's increased Loading Composable is recomposed.
when a result comes Loading Composable exits composition
Header(result)
Body(result)
enters composition
Also, Composable blocks are not necessarily need to be UI related either. You can create a Composable block for with LaunchedEffect to display a SnackBar for instance.
if (count > 0 && count <5) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// if statement is false, and only start when statement is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar("count $count")
}
}
This block will enter composition when count is bigger than 0 and stay in composition while count is less than 5 but since it's LaunchedEffect it will trigger once but if count reaches 5 faster than Snackbar duration Snackbar gets canceled because block leaves composition.
You can check when a Composable enters and exits composition with DisposableEffect
And you can check this article for deeper analysis.
https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd

How to make a button a menu trigger in Jetpack Compose?

I tried adding an IconButton() and wanted to click the button to turn it on or off (while also clicking elsewhere to dismiss it)
But a funny qustion happend.
The trigger button is included in "Other Places", when I click the button to close the menu, this triggers onDismissRequest() first, and then triggers the button's onClick(), which makes me unable to close the menu (when clicked will instantly close and then open again)
Scaffold(
...
topBar = {
TopAppBar(
...
actions = {
var menuExpanded by remember { mutableStateOf(false) }
Box {
IconButton(onClick = { menuExpanded = !menuExpanded}) {
Icon(
painter = painterResource(id = R.drawable.menu),
contentDescription = "menu",
tint = white
)
}
DropdownMenu(
expanded = menuExpanded,
properties = PopupProperties(),
onDismissRequest = { menuExpanded = false }
) {
// items
}
}
}
)
}
) { ... }
I know I can set Modifier.offset() so that menu masks the button, but I don't want to do that.
What should I do?
This is what PopupProperties.focusable is for: true value prevents other views from getting tapped while the menu is open. By the way, this is the default value if you don't specify properties option.
DropdownMenu(
expanded = menuExpanded,
properties = PopupProperties(focusable = true),
onDismissRequest = { menuExpanded = false }
) {