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

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 }
) {

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?

How to handle key events during TextField editing in Compose?

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
}
)

How can I present an overlay above the keyboard in SwiftUI?

I have a screen in my app that automatically displays a keyboard when the screen is presented. The screen allows users to enter their mobile number. In order to select a dialling code, the user needs to tap a button which will then trigger the presentation of an overlay.
Problem
The overlay is being presented, but it's showing up behind the currently present keyboard.
Question
How can I make this overlay be the very top view?
There is no way for me to use the zIndex modifier on the keyboard for obvious reasons. I'm wondering if there is a way to make the overlay the top view when it's about to be presented, or if the overlay can be added to the window.
Thanks in advance
You should probably only have one source of input at any given time — either the keyboard should be presented to enter a number, or the overlay should be presented to pick the dialing code, not both. Here's an example which hides the keyboard when overlay appears, and vice versa. (Keyboard dismissal code from this answer.)
struct ContentView: View {
#State private var number = ""
#State private var showingOverlay = false
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
// Just here to force ZStack to use the whole screen
Rectangle()
.fill(Color.clear)
VStack(alignment: .leading) {
Button("Select Dialing Code") {
UIApplication.shared.endEditing()
self.showingOverlay = true
}
TextField("Enter your number", text: self.$number, onEditingChanged: {
if $0 { self.showingOverlay = false }
})
.keyboardType(.phonePad)
}
Overlay(showing: self.$showingOverlay)
.frame(height: 400)
.offset(x: 0, y: proxy.size.height + (self.showingOverlay ? -300 : 100))
.animation(.easeInOut)
}
}
}
}
struct Overlay: View {
#Binding var showing: Bool
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color(.systemGray4))
Button("Dismiss") {
self.showing = false
}
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

Transition with keepScrollPosition and navigateBack

We are using Durandal for our SPA application and came to a, in my opinion, common use case. We have two pages: one page is a list of entities (with filters, sorting, virtual scroll) and another is detail preview of an entity. So, user is on list page and set a filter and a list of results comes out. After scrolling a little bit down user notice an entity which he/she would like to see details for. So clicking on a proper link user is navigated to details preview page.
After "work finished" on preview page user click back button (in app itself or browser) and he/she is back on the list page. However, default 'entrance' transition scroll the page to the top and not to the position on list where user pressed preview. So in order to 'read' list further user have to scroll down where he/she was before pressing preview.
So I started to create new transition which will for certain pages (like list-search pages) keep the scroll position and for other pages (like preview or edit pages) scroll to top on transition complete. And this was easy to do however, I was surprised when I noticed that there are strange behavior on preview pages when I hit navigateBack 'button'. My already long story short, after investigation I found out that windows.history.back is completing earlier then the transition is made and this cause that preview pages are scrolled automatically down to position of previous (list) page when back button is hit. This scrolling have a very unpleasant effect on UI not mentioning that it is 'total catastrophe' for my transition.
Any idea or suggestion what could I do in this case?
Here is the code of transition. It is just a working copy not finished yet as far as I have this problem.
define(['../system'], function (system) {
var fadeOutDuration = 100;
var scrollPositions = new Array();
var getScrollObjectFor = function (node) {
var elemObjs = scrollPositions.filter(function (ele) {
return ele.element === node;
});
if (elemObjs.length > 0)
return elemObjs[0];
else
return null;
};
var addScrollPositionFor = function (node) {
var elemObj = getScrollObjectFor(node);
if (elemObj) {
elemObj.scrollPosition = $(document).scrollTop();
}
else {
scrollPositions.push({element: node, scrollPosition: $(document).scrollTop()});
}
};
var scrollTransition = function (parent, newChild, settings) {
return system.defer(function (dfd) {
function endTransition() {
dfd.resolve();
}
function scrollIfNeeded() {
var elemObj = getScrollObjectFor(newChild);
if (elemObj)
{
$(document).scrollTop(elemObj.scrollPosition);
}
else {
$(document).scrollTop(0);
}
}
if (!newChild) {
if (settings.activeView) {
addScrollPositionFor(settings.activeView);
$(settings.activeView).fadeOut(fadeOutDuration, function () {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
});
} else {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
}
} else {
var $previousView = $(settings.activeView);
var duration = settings.duration || 500;
var fadeOnly = !!settings.fadeOnly;
function startTransition() {
if (settings.cacheViews) {
if (settings.composingNewView) {
ko.virtualElements.prepend(parent, newChild);
}
} else {
ko.virtualElements.emptyNode(parent);
ko.virtualElements.prepend(parent, newChild);
}
var startValues = {
marginLeft: fadeOnly ? '0' : '20px',
marginRight: fadeOnly ? '0' : '-20px',
opacity: 0,
display: 'block'
};
var endValues = {
marginRight: 0,
marginLeft: 0,
opacity: 1
};
$(newChild).css(startValues);
var animateOptions = {
duration: duration,
easing : 'swing',
complete: endTransition,
done: scrollIfNeeded
};
$(newChild).animate(endValues, animateOptions);
}
if ($previousView.length) {
addScrollPositionFor(settings.activeView);
$previousView.fadeOut(fadeOutDuration, startTransition);
} else {
startTransition();
}
}
}).promise();
};
return scrollTransition;
});
A simpler approach could be to store the scroll position when the module deactivates and restore the scroll on viewAttached.
You could store the positions in some global app variable:
app.scrollPositions = app.scrollPositions || {};
app.scrollPositions[system.getModuleId(this)] = theCurrentScrollPosition;