TornadoFX get reference to a tab and get its Stage - kotlin

How can I get a reference to the first tab? And furthermore how do I get its Stage?
class MainApp : App() {
override val primaryView = MainView::class
class MainView : View() {
override val root = VBox()
init {
with(root) {
tabpane {
tab("Report") {
hbox {
// TODO Want a reference to this tab here.
// Ideally something like tab.getStage()
this += Button("Hello 1")
}
}
tab("Data Entry") {
hbox {
this += Button("Hello 2")
}
}
}
}
}
}
}

Quickly: I've seen a lot of your posts here and they're pretty basic questions. These are things you could figure out on your own if you did your own digging. I'd recommend at least looking at the official guide to get a good grasp on most of what you need to know. Then, check out other posts on here to see if they've been answered already.
But to answer your question:
class MainView : View() {
override val root = vbox {
tabpane {
tab("Report") {
hbox {
val tab = this#tab //Here is your tab
button("Hello 1")
}
}
tab("Data Entry") {
hbox {
button("Hello 2")
}
}
}
}
}
Again, I would urge you to look at the guide, as you missed some helpful building tools (see how I built the buttons? see how I moved the root out of init?). I'd hate for you to code more than you need to then realize you could've done less work if you had known how.
Also: Tabs don't have references to stages. They just inherit Styleable and EventTarget, they're not like Views or Fragments.

Related

Lazy Column is blinking when navigating with compose navigation

I set up navigation, pagination and use flow to connect ui with model. If simplify, my screen code looks like this:
#Composable
MainScreen() {
val listState = rememberLazyListState()
val lazyItems = Pager(PagingConfig(...)) { ... }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
LazyColumn(state = listState) {
items(lazyItems, key = { it.id }) { ... }
}
}
And here is my NavHost code:
NavHost(navController, startDestination = "mainScreen") {
composable("mainScreen") {
MainScreen()
}
}
But when i navigate back to MainScreen from another screen or just opening the drawer, data is loaded from DataSource again and i see noticeable blink of LazyColumn.
How to avoid reloading data?
Your code gives me the following error for cachedIn:
Flow operator functions should not be invoked within composition
You shouldn't ignore such warnings.
During transition Compose Navigation recomposes both disappearing and appearing views many times. This is the expected behavior.
And your code creates a new Pager with a new flow on each recomposition, which is causing the problem.
The easiest way to solve it is using remember: it'll cache the pager flow between recompositions:
val lazyItems = remember {
Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
}
But it'll still be reset during configuration change, e.g. device rotation. The best way to prevent this is moving this logic into a view model:
class MainScreenViewModel : ViewModel() {
val pagingFlow = Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
}
#Composable
fun MainScreen(
viewModel = viewModel<MainScreenViewModel>()
) {
val lazyItems = viewModel.pagingFlow.collectAsLazyPagingItems()
}

TornadoFX: Error while updating label inside runAsync

I tried to implement a chat application using socket connection in Kotlin and TornadoFX library to make the GUI.
The problem comes when I try to launch the client because it keeps waiting a message from the Server although I put that code that update the label and receive the message inside a runAsync. I red the TornadoFX documentation and saw youtube videos but I cannot come to the solution.
I know that the issue is that the program is stuck in that block but can't figure how to do it.
class MyFirstView: View("Chat"){
var input: TextField by singleAssign()
var test = SimpleStringProperty()
val client: Client by inject()
init {
client.connect()
val t = thread(true) {
while (true) {
random = client.getMessage()
println(random)
Platform.runLater { test.set(random) }
}
}
}
override val root = vbox {
hbox {
label(test) {
bind(test)
}
}
hbox {
label("Write here some text")
input = textfield()
}
hbox {
button("Send") {
action{
client.writer.println(input.text)
}
}
}
}
}
You can only update UI elements on the UI thread, so if you want to manipulate the UI from a background thread, you need to wrap that particular code in runLater { }.
On another note, you shouldn't manipulate the text of the textfield or store ui element references with singleAssign. Instead you should bind your textfield to a StringProperty and manipulate the value instead. This is covered in the guide, so check it out :)

How to improve my code to show a data array in a table, with TornadoFX?

This is my way to display an array of data:
private val data = observableArrayList(
arrayOf("AAA", "111"),
arrayOf("BBB", "222"),
arrayOf("CCC", "333")
)
class HelloWorld : View() {
override val root = tableview<Array<String>>(data) {
column("name") { cellDataFeatures: TableColumn.CellDataFeatures<Array<String>, String> ->
SimpleStringProperty(cellDataFeatures.value[0])
}
column("value") { cellDataFeatures: TableColumn.CellDataFeatures<Array<String>, String> ->
SimpleStringProperty(cellDataFeatures.value[1])
}
}
}
It works but the code is quite complex. Is there any better way to do it?
(Maybe define a class to hold the data will make it much simpler, but I just want to test some uncommon cases)
Update:
A complete demo project for this: https://github.com/javafx-demos/tornadofx-tableview-array-data-demo
Here is a simpler way of defining your columns:
class HelloWorld : View() {
override val root = tableview(data) {
column<Array<String>, String>("name", { it.value[0].toProperty() })
column<Array<String>, String>("value", { it.value[1].toProperty() })
}
}
That said, using a specialized data structure would yield less headache :)
An alternative approach would be to configure just the cell item type and then a value factory:
column("name", String::class) {
value { it.value[0] }
}
column("value", String::class) {
value { it.value[1] }
}

TableView and Fragment to edit Details with tornadofx

I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}

Anko 0.8 - unresolved lparams reference

The main question is: is lparams simply gone from Anko, or am I doing something terribly wrong? The following snippet fails to compile:
verticalLayout {
}.lparams(width = matchParent, height = matchParent) {
topMargin = dip(10)
}
While this works without any problems:
verticalLayout {
layoutParams = LinearLayout.LayoutParams(matchParent, matchParent).apply {
topMargin = dip(10)
}
}
I wouldn't mind the second option too much, but you have to specify the layout type when generating the params, which can get a bit tiresome (and is also more brittle than the original solution).
I haven't found anything on the Anko GitHub page, the changelog, or by skimming recent commits. Here's the full UI class for reference:
class ReviewsFragmentUi(ctx: AnkoContext<ReviewsFragment>) : AnkoComponent<ReviewsFragment> {
override fun createView(ui: AnkoContext<ReviewsFragment>) = ui.apply {
verticalLayout {
layoutParams = LinearLayout.LayoutParams(matchParent, matchParent).apply {
topMargin = dip(10)
}
}
}.view
}
Relevant Gradle entries (I'm using Kotlin 1.0.0-beta-3595):
ext.versions = [
anko : '0.8.1',
]
compile "org.jetbrains.anko:anko-common:$versions.anko",
compile "org.jetbrains.anko:anko-sdk21:$versions.anko",
compile "org.jetbrains.anko:anko-support-v4:$versions.anko",
compile "org.jetbrains.anko:anko-design:$versions.anko",
compile "org.jetbrains.anko:anko-appcompat-v7:$versions.anko",
compile "org.jetbrains.anko:anko-cardview-v7:$versions.anko",
compile "org.jetbrains.anko:anko-recyclerview-v7:$versions.anko",
compile "org.jetbrains.anko:anko-gridlayout-v7:$versions.anko",
As a follow-up question: if lparams is indeed gone, then is there a more elegant replacement than what I'm already doing?
Apparently lparams is still there, but cannot be used as an extension function for the outermost layout:
So the following code fails:
override fun createView(ui: AnkoContext<ReviewsFragment>) = ui.apply {
verticalLayout {
// Layout elements here
}.lparams {
// Layout params here
}
}.view
But this compiles fine:
override fun createView(ui: AnkoContext<ReviewsFragment>) = ui.apply {
verticalLayout {
lparams {
// Layout params here
}
// Layout elements here
verticalLayout { }.lparams {
// lparams works fine if there is a parent layout
}
}
}.view
It's worth noting that using the non-tailing version of lparams is discouraged for inner layouts: it will create the wrong sublass of LayoutParams when the nested layouts are of different types. For a complete discussion, see this GitHub Issue.
Why don't you use the most recent way to write createView() method?
I think the following solves your problem:
override fun createView(ui: AnkoContext<ReviewsFragment>) : View = with(ui) {
return verticalLayout {
// Layout elements here
}.lparams {
// Layout params here
}
}