Progressbar TaskStatus does listen to all async runs - kotlin

I made a small example for you guyes to see what i mean.
By Running it you will see that on clicking into the Yellow area the progress bar is shown... But i only want it to be shown if the Green area is clicked.
Am i doing sth wrong or is this actually an expected behavior?
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.VBox
import tornadofx.*
class RunAsyncExample : View() {
override val root = VBox()
private val runAsyncOne: RunAsyncOne by inject()
private val runAsyncTwo: RunAsyncTwo by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 300.0
add(runAsyncOne)
add(runAsyncTwo)
}
}
}
class RunAsyncOne : View() {
override val root = AnchorPane()
init {
with(root) {
var checker = false
minWidth = 400.0
minHeight = 150.0
style {
backgroundColor += c("YELLOW")
}
setOnMouseClicked {
checker = !checker
runAsync {
while (checker) {
Thread.sleep(100)
println("AsyncOne")
}
}
}
}
}
}
class RunAsyncTwo : View() {
override val root = VBox()
private val status: TaskStatus by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync {
var b = 0.0
while (b < 100.0) {
b+=1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen { status.running }
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}
}
When i click into the green area:
When i click into the yellow area while the AsyncTwo is running,
the progress bar is changing but i dont want that...

When you inject a TaskStatus object into your View, you will get a "global" TaskStatus object that by default will report status of any async calls. What you want is to create a local TaskStatus object for the RunAsyncTwo View and pass that to the runAsync call.
If you don't pass a TaskStatus object to runAsync, the default for your scope will be used, hence the behavior you're seeing. Here is an example with a local TaskStatus object for the second view. Please also note that I'm using a more sensible syntax for defining the root node, you should absolutely pick this pattern up :)
class RunAsyncTwo : View() {
private val status = TaskStatus()
override val root = vbox {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync(status) {
var b = 0.0
while (b < 100.0) {
b += 1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen(status.running)
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}

Related

Why my return value is coming as NaN or default in kotlin?

For a while, I want to convert the entered currencies to each other and show them on the other screen, but when I check the view model with println, the result I can see is NaN when I make viewmodel.result in the ui. What is the reason for this and how can I solve it?
my ui
If the user presses oncofirm on the button, the operations in the view model are performed.
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm = {
viewModel.getConversionRateByCurrency()
viewModel.onDismissClick()
//viewModel.calculate()
println(viewModel.resultState)
With println(viewModel.resultState) comes 0.0
but when I press the button for the second time and say confirm, then the correct result comes.
my view model
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val exchangeInsertUseCase: InsertExchangeUseCase,
private val exchangeGetAllUseCase: GetAllExchangeUseCase,
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var isDialogShown by mutableStateOf(false)
private set
var dropDownMenuItem1 by mutableStateOf("")
var dropDownMenuItem2 by mutableStateOf("")
var outLineTxtFieldValue by mutableStateOf(TextFieldValue(""))
var firstConversionRate by mutableStateOf(0.0)
var secondConversionRate by mutableStateOf(0.0)
var resultState by mutableStateOf(0.0)
fun onConfirmClick() {
isDialogShown = true
}
fun onDismissClick() {
isDialogShown = false
}
fun check(context: Context): Boolean {
if (outLineTxtFieldValue.text.isNullOrEmpty() || dropDownMenuItem1 == "" || dropDownMenuItem2 == "") {
Toast.makeText(context, "please select a value and currency ", Toast.LENGTH_LONG).show()
return false
}
return true
}
fun getConversionRateByCurrency() {
viewModelScope.launch {
val firstRate = async {
getConversionRateByCurrencyUseCase.getConversionRateByCurrency(dropDownMenuItem1)
}
val secondRate = async {
getConversionRateByCurrencyUseCase.getConversionRateByCurrency(dropDownMenuItem2)
}
firstConversionRate = firstRate.await()
secondConversionRate = secondRate.await()
delay(200L)
val result = async {
calculate()
}
resultState = result.await()
}
}
private fun calculate(): Double {
if (!firstConversionRate.equals(0.0) && !secondConversionRate.equals(0.0)) {
val amount = outLineTxtFieldValue.text.toInt()
val resultOfCalculate = (amount / firstConversionRate) * secondConversionRate
return resultOfCalculate
}
return 1.1
}
}
I can see the value in the view model but not the ui. Also, I do a lot of checking with if and 0.0 because I couldn't get out of it, so I followed a method like this because I couldn't solve the problem. Anyone have a better idea?

How do I display a new image in tornadofx imageview?

I want to display a WritableImage in imageview, but I want that image to change when the user loads in a new file from the file browser. I know that there is a bind() function for strings that change over time, but I could not find a similar option for images. I could solve the problem for images that are the same size as the default loaded one (with writing through the pixels), but that only works if they are the same size, since I cant modify the size of the image that I displayed.
My Kotlin code so far:
class PhotoView : View("Preview") {
val mainController: mainController by inject()
override val root = hbox {
imageview{
image = mainController.moddedImg
}
hboxConstraints {
prefWidth = 1000.0
prefHeight = 1000.0
}
}
class ControlView: View(){
val mainController: mainController by inject()
override val root = hbox{
label("Controls")
button("Make BW!"){
action{
mainController.makeBW()
}
}
button("Choose file"){
action{
mainController.setImage()
mainController.update()
}
}
}
}
class mainController: Controller() {
private val ef = arrayOf(FileChooser.ExtensionFilter("Image files (*.png, *.jpg)", "*.png", "*.jpg"))
private var sourceImg=Image("pic.png")
var moddedImg = WritableImage(sourceImg.pixelReader, sourceImg.width.toInt(), sourceImg.height.toInt())
fun setImage() {
val fn: List<File> =chooseFile("Choose a photo", ef, FileChooserMode.Single)
sourceImg = Image(fn.first().toURI().toString())
print(fn.first().toURI().toString())
}
fun makeBW() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color.grayscale())
}
}
}
fun update() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color)
}
}
}
}
ImageView has a property for the image that you can bind:
class PhotoView : View("Preview") {
val main: MainController by inject()
val root = hbox {
imageview { imageProperty().bind(main.currentImageProperty) }
...
}
...
}
class MainController : Controller() {
val currentImageProperty = SimpleObjectProperty<Image>(...)
var currentImage by currentImageProperty // Optional
...
}
From there, any time you set the currentImage in MainController, it will update in the PhotoView.

Adding TornadoFX object removes other elements of stage

I was playing around with TornadoFX and wanted to add a horizontal line to my screen, to see how it works. I added it to my code as follows:
private val menuView: MenuView by inject()
private val controller: MainController by inject()
override val root = borderpane {
top = menuView.root
style {
backgroundColor += Color.WHITE
}
val data = controller.getData()
center {
for (i in 0 until data.count()) {
val values = data[i]
datagrid(values) {
if (data.count() > 0) {
cellWidth = (8.0 * (values.maxBy { it.root.count() }!!.root.count()))
}
cellHeight = 20.0
horizontalCellSpacing = 0.0
maxCellsInRow = controller.maxNum
}
}
line {
startY = 3000.0
endY = 3000.0
startX = 500.0
endX = 5000.0
}
}
}
It seems that adding the line inside the center component leads to it being the only thing rendered. The desired result is achieved by replacing the borderpane with a stackpane, as follows:
private val menuView: MenuView by inject()
private val controller: MainController by inject()
override val root = stackpane {
style {
backgroundColor += Color.WHITE
}
val data = controller.getData()
for (i in 0 until data.count()) {
val values = data[i]
datagrid(values) {
if (data.count() > 0) {
cellWidth = (8.0 * (values.maxBy { it.root.count() }!!.root.count()))
}
cellHeight = 20.0
horizontalCellSpacing = 0.0
maxCellsInRow = controller.maxNum
}
}
line {
startY = 3000.0
endY = 3000.0
startX = 500.0
endX = 5000.0
}
}
However, this removes the menu bar from the top, which I also wanted to keep. Is there a way to have both?
Thanks to #Slaw for helping me with this. The solution was actually quite simple. In the center part of the solution, instead of just building all the nodes there, I had to embed them within a StackPane. This is because center can only take a single node.
Therefore, the solution looks like this:
private val menuView: MenuView by inject()
private val controller: MainController by inject()
override val root = borderpane {
top = menuView.root
style {
backgroundColor += Color.WHITE
}
val data = controller.getData()
center {
stackpane {
for (i in 0 until data.count()) {
val values = data[i]
datagrid(values) {
if (data.count() > 0) {
cellWidth = (8.0 * (values.maxBy { it.root.count() }!!.root.count()))
}
cellHeight = 20.0
horizontalCellSpacing = 0.0
maxCellsInRow = controller.maxNum
}
}
line {
startY = 3000.0
endY = 3000.0
startX = 500.0
endX = 5000.0
}
}
}
}

Why does my tornadoFX ObservableList not receive updates?

I have a simple tornadoFX program that generates some circles in random locations on the screen. However, none of the circles get drawn. I've added some debug code to print a line when a circle is drawn, and it only prints once.
I would expect circles to appear at 100ms intervals, as well as when I click the "Add actor" button.
private const val WINDOW_HEIGHT = 600
private const val WINDOW_WIDTH = 1024
fun main(args: Array<String>) {
Application.launch(MainApp::class.java, *args)
}
class MainApp : App(WorldView::class, Stylesheet::class)
data class Actor(val x: Double, val y: Double)
class WorldView: View("Actor Simulator") {
override val root = VBox()
private val actors = ArrayList<Actor>(0)
init {
tornadofx.runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
actors.add(Actor(x, y))
Thread.sleep(100)
}
}
}
init {
with(root) {
stackpane {
group {
bindChildren(actors.observable()) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
also {
println("drew circle")
}
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}
}
}
Oddly, if I put a breakpoint during the circle draw code, circles will draw and the debug line will print.
Some observations:
Calling someList.observable() will create an observable list backed by the underlying list, but mutations on the underlying list will not emit events. You should instead initialize actors as an observable list right away.
Access to an observable list must happen on the UI thread, so you need to wrap mutation calls in runLater.
For people trying to run your example - you didn't include a stylesheet, but references one in your App subclass, so the IDEA will most probably import the TornadoFX Stylesheet class. This will not end well :)
The also call has no effect, so I removed it.
I updated your code to best practices here and there, for example with regards to how to create the root node :)
Updated example taking these points into account looks like this:
private const val WINDOW_HEIGHT = 600.0
private const val WINDOW_WIDTH = 1024.0
class MainApp : App(WorldView::class)
data class Actor(val x: Double, val y: Double)
class WorldView : View("Actor Simulator") {
private val actors = FXCollections.observableArrayList<Actor>()
override fun onDock() {
runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
runLater {
actors.add(Actor(x, y))
}
Thread.sleep(100)
}
}
}
override val root = stackpane {
setPrefSize(WINDOW_WIDTH, WINDOW_HEIGHT)
group {
bindChildren(actors) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
println("drew circle")
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}

How do install a click handler on a dynamic listview (in tornadofx)

My application needs to permit additions to the listview. I've figured out how I can dynamically add to a listview by using observableArrayList. If I click on the button, an item gets added to the list and displayed.
Now I'm struggling to add a click handler (I want to handle the event that happens when someone clicks on any item within the list view). Where do I do this?
Here is my code.
package someapp
import javafx.collections.FXCollections
import javafx.geometry.Pos
import javafx.scene.layout.VBox
import javafx.scene.text.FontWeight
import tornadofx.*
class MyApp : App(HelloWorld::class) {
}
class HelloWorld : View() {
val leftSide: LeftSide by inject()
override val root = borderpane {
left = leftSide.root
}
}
class LeftSide: View() {
var requestView: RequestView by singleAssign()
override val root = VBox()
init {
with(root) {
requestView = RequestView()
this += requestView
this += button("Add Item") {
action {
requestView.responses.add( Request( "example.com",
"/foo/bar",
"{ \"foo\" : \"bar\"}".toByteArray()))
}
}
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>(
)
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray) {
}
To configure a callback when an item in a ListView is selected, use the onUserSelect callback:
onUserSelect {
information("You selected $it")
}
You can optionally pass how many clicks constitutes a select as well, default is 2:
onUserSelect(1) {
information("You selected $it")
}
You are using some outdated constructs in your code, here is an updated version converted to best practices :)
class MyApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = borderpane {
left(LeftSide::class)
}
}
class LeftSide : View() {
val requestView: RequestView by inject()
override val root = vbox {
add(requestView)
button("Add Item").action {
requestView.responses.add(Request("example.com",
"/foo/bar",
"""{ "foo" : "bar"}""".toByteArray()))
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>()
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
onUserSelect(1) {
information("You selected $it")
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray)