Giter Club home page Giter Club logo

tornadofx's Introduction

TornadoFX Logo

TornadoFX

JavaFX Framework for Kotlin

(This project is no longer maintained)

Travis CI Maven Central Apache License

Important: TornadoFX is not yet compatible with Java 9/10

Oracle is intending to decouple JavaFX from the JDK. We will wait until the decoupled JavaFX is available and stable before upgrading TornadoFX to support it. As of now there is little value and significant effort involved in updating to JDK 9/10, while there will be an enormous value in updating to the decoupled version.

Features

  • Supports both MVC, MVP and their derivatives
  • Dependency injection
  • Type safe GUI builders
  • Type safe CSS builders
  • First class FXML support
  • Async task execution
  • EventBus with thread targeting
  • Hot reload of Views and Stylesheets
  • OSGi support
  • REST client with automatic JSON conversion
  • Zero config, no XML, no annotations

Important version note

TornadoFX requires Kotlin 1.1.2 and jvmTarget 1.8. Make sure you update your IDE plugins (Kotlin + TornadoFX).

After updating IntelliJ IDEA, make sure your Kotlin target version is 1.1 (Project Settings -> Modules -> Kotlin -> Language Version / API Version)

Remember to update your build system to configure the jvmTarget as well.

For Maven, you add the following configuration block to kotlin-maven-plugin:

<configuration>
    <jvmTarget>1.8</jvmTarget>
</configuration>

For Gradle, it means configuring the kotlinOptions of the Kotlin compilation task:

compileKotlin {
    kotlinOptions.jvmTarget= "1.8"
}

Failing to do so will yield errors about the compiler not being able to inline certain calls.

You also need a full rebuild of your code after a version upgrade. If you run into trouble, try to clean caches and restart IDEA (File -> Invalidate caches / Restart).

Getting started

Generate a quickstart application with Maven

mvn archetype:generate -DarchetypeGroupId=no.tornado \
  -DarchetypeArtifactId=tornadofx-quickstart-archetype \
  -DarchetypeVersion=1.7.20

Add TornadoFX to your project

Maven

<dependency>
    <groupId>no.tornado</groupId>
    <artifactId>tornadofx</artifactId>
    <version>1.7.20</version>
</dependency>

Gradle

implementation 'no.tornado:tornadofx:1.7.20'

Snapshots are published to Sonatype

Configure your build environment to use snapshots if you want to try out the latest features:

 <repositories>
   <repository>
     <id>snapshots-repo</id>
     <url>https://oss.sonatype.org/content/repositories/snapshots</url>
     <releases><enabled>false</enabled></releases>
     <snapshots><enabled>true</enabled></snapshots>
   </repository>
 </repositories>

Snapshots are published every day at GMT 16:00 if there has been any changes.

What does it look like? (Code snippets)

Create a View

class HelloWorld : View() {
    override val root = hbox {
        label("Hello world")
    }
}

Start your application and show the primary View and add a type safe stylesheet

import javafx.scene.text.FontWeight
import tornadofx.*

class HelloWorldApp : App(HelloWorld::class, Styles::class)

class Styles : Stylesheet() {
    init {
        label {
            fontSize = 20.px
            fontWeight = FontWeight.BOLD
            backgroundColor += c("#cecece")
        }    
    }    
}

Start app and load a type safe stylesheet

Use Type Safe Builders to quickly create complex user interfaces

class MyView : View() {
    private val persons = FXCollections.observableArrayList(
            Person(1, "Samantha Stuart", LocalDate.of(1981,12,4)),
            Person(2, "Tom Marks", LocalDate.of(2001,1,23)),
            Person(3, "Stuart Gills", LocalDate.of(1989,5,23)),
            Person(3, "Nicole Williams", LocalDate.of(1998,8,11))
    )

    override val root = tableview(persons) {
        column("ID", Person::id)
        column("Name", Person::name)
        column("Birthday", Person::birthday)
        column("Age", Person::age)
        columnResizePolicy = SmartResize.POLICY
    }
}

RENDERED UI

Create a Customer model object that can be converted to and from JSON and exposes both a JavaFX Property and getter/setter pairs:

import tornadofx.getValue
import tornadofx.setValue

class Customer : JsonModel {
    val idProperty = SimpleIntegerProperty()
    var id by idProperty

    val nameProperty = SimpleStringProperty()
    var name by nameProperty

    override fun updateModel(json: JsonObject) {
        with(json) {
            id = int("id") ?: 0
            name = string("name")
        }
    }

    override fun toJSON(json: JsonBuilder) {
        with(json) {
            add("id", id)
            add("name", name)
        }
    }
}

Create a controller which downloads a JSON list of customers with the REST api:

class HelloWorldController : Controller() {
    val api : Rest by inject()
    
    fun loadCustomers(): ObservableList<Customer> = 
        api.get("customers").list().toModel() 
}

Configure the REST API with a base URI and Basic Authentication:

with (api) {
    baseURI = "http://contoso.com/api"
    setBasicAuth("user", "secret")
}

Load customers in the background and update a TableView on the UI thread:

runAsync {
    controller.loadCustomers()
} ui {
    customerTable.items = it
}

Load customers and apply to table declaratively:

customerTable.asyncItems { controller.loadCustomers() }

Define a type safe CSS stylesheet:

class Styles : Stylesheet() {
    companion object {
        // Define css classes
        val heading by cssclass()
        
        // Define colors
        val mainColor = c("#bdbd22")
    }

    init {
        heading {
            textFill = mainColor
            fontSize = 20.px
            fontWeight = BOLD
        }
        
        button {
            padding = box(10.px, 20.px)
            fontWeight = BOLD
        }

        val flat = mixin {
            backgroundInsets += box(0.px)
            borderColor += box(Color.DARKGRAY)
        }

        s(button, textInput) {
            +flat
        }
    }
}

Create an HBox with a Label and a TextField with type safe builders:

hbox {
    label("Hello world") {
        addClass(heading)
    }
    
    textfield {
        promptText = "Enter your name"
    }
}

Get and set per component configuration settings:

// set prefWidth from setting or default to 200.0
node.prefWidth(config.double("width", 200.0))

// set username and age, then save
with (config) {
    set("username", "john")
    set("age", 30)
    save()
}

Create a Fragment instead of a View. A Fragment is not a Singleton like View is, so you will create a new instance and you can reuse the Fragment in multiple ui locations simultaneously.

class MyFragment : Fragment() {
    override val root = hbox {
    }
}

Open it in a Modal Window:

find<MyFragment>().openModal()

Lookup and embed a View inside another Pane in one go

add<MyFragment>()

Inject a View and embed inside another Pane

val myView: MyView by inject()
 
init {
    root.add(myFragment)
}

Swap a View for another (change Scene root or embedded View)

button("Go to next page") {
    action {
        replaceWith<PageTwo>(ViewTransition.Slide(0.3.seconds, Direction.LEFT)
    }
}

Open a View in an internal window over the current scene graph

button("Open") {
    action {
        openInternalWindow<MyOtherView>()
    }
}

tornadofx's People

Contributors

alexp11223 avatar anidotnet avatar aymen083 avatar bekwam avatar carltonwhitehead avatar daanvandenbosch avatar demol-ee avatar dominiks avatar drmoose avatar edvin avatar gbui avatar genobis avatar gtnarg avatar hastebrot avatar holdyourwaffle avatar kastork avatar lordraydenmk avatar nekoinemo avatar nimakro avatar picoworm avatar pvdberg1998 avatar ronsmits avatar ruckustboom avatar sackcastellon avatar sanchezmk avatar sleonidy avatar stubborn-me avatar thomasnield avatar tieskedh avatar zshamrock avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tornadofx's Issues

Strange Behaviors with tabpane

I'm getting some odd behaviors with the tabpane, and I might be using it incorrectly.

nadofx.*

class MainApp : App() {
    override val primaryView = MainView::class

    class MainView : View() {
        override val root = VBox()

        init {
            with(root) {
                tabpane {
                    tab("Report") {
                        hbox {
                            this += Button("Hello 1")
                        }
                    }
                    tab("Data Entry") {
                        hbox {
                            this += Button("Hello 2")
                        }
                    }
                }
            }
        }
    }
}

Whatever controls I add to a tab, they are visible on all tabs. Can you validate if I'm doing something incorrectly?

Issues with Property Calls and TableView

I'm doing some home projects and testing out some functionality. I've got a really odd compilation error with IDEA when using some Property delegates with a TableView.

Here is my domain class

class ParsedTransaction(
        val transactionDate: LocalDate,
        val memo: String,
        val amount: BigDecimal

    ) {

    var categoryId: Int by property<Int>()
    var categoryIdProperty = getProperty(ParsedTransaction::categoryId)

    var accountId: Int by property<Int>()
    var accountIdProperty = getProperty(ParsedTransaction::accountId)

    var isTransfer: Boolean by property(false)
    var isTransferProperty = getProperty(ParsedTransaction::isTransfer)

    fun writeToDb() = db.update("INSERT INTO HARD_TRANSACTION (CATEGORY_ID,ACCOUNT_ID,TRANSACTION_DATE,AMOUNT,MEMO,TRANSFER_IND) VALUES (?,?,?,?,?,?)")
        .parameters(categoryId,accountId,transactionDate,amount,memo,isTransfer)
        .count()
}

Here is my TableView

tableview<ParsedTransaction>() {
                isEditable = true
                column("TRANS DATE",ParsedTransaction::transactionDate)
                column("MEMO",ParsedTransaction::memo)
                column("AMOUNT",ParsedTransaction::amount)
                column("ACCOUNT ID", ParsedTransaction::accountIdProperty)
                column("IS TRANSFER",ParsedTransaction::isTransferProperty)
            }

It is a little difficult to get a self-contained runnable example. But does anything stand out that might cause the problem? stacktrace is attached also...

stacktrace.txt

FXCollections Declarations

Small ask that I can implement since you all are busy with CSS DSL stuff: do you suppose there is a way we can streamline the use of FX collections a little bit in a Kotlin-esque way?

Instead of this...

val greekLetters = FXCollections.observableArrayList("Alpha","Beta",
        "Gamma","Delta","Epsilon")

Why not this?

val greekLetters = observableList("Alpha","Beta","Gamma","Delta","Epsilon")

Maybe we can add extension functions to non-FX collections as well to turn them into FX collections too?

//using Google Guava collections
val greekLetters = ImmutableList.of("Alpha","Beta",
        "Gamma","Delta","Epsilon")

//turn into ObservableList
val fxGreekLetters = greekLetters.toUnmodifiableObservableList()

Chart Builders

I may take this one but I'll probably save it for a future release.

It may be helpful to create builders for JavaFX Charts.

I think it may have some similarities with how TreeView and TreeTableView were handled, but it should not be as hard.

Improve documentation

Some of the documentation pages are becoming rather lengthy and might be hard to follow, like for example the Type Safe Builders page. We need to think about a better structure, and possibly a better format for the documentation.

Some options:

  1. Restructure/split the wiki pages
  2. Create a dedicated web page for the project with documentation, maybe something like the JacpFX Documentation
  3. Start thinking about/planning a book
  4. Create a larger sample application that touch as much of the functionality as possible. This could also possibly be the basis for a book.

Inconsistent Builder Naming Conventions?

Was doing some documentation for the guide and I saw a few builders that were not all lowercase like the rest of the controls.

progressBar()
progressIndicator()
titledPane()

@edvin I think you said that you'd prefer all builders to be lowercase. Do these count?

Update documentation for release 1.3.1

I started a new page called Properties in the Wiki, where I just documented singleAssign(). I'll also add some documentation on builders later. I think we can show several practical examples using the new ones we developed.

Extending tornadofx.View or tornadofx.Component.View

Can't understand issue because my view class extend abstract View thas located in Component.kt
But on compile get the message below:

Kotlin: Type of 'primaryView' is not a subtype of the overridden property 'public abstract val primaryView: kotlin.reflect.KClass defined in tornadofx.App'

Maybe somethig wrong with artifacts that gradle get for me from MavenCentral and something are not 'up-to-date'
(versions compile 'no.tornado:tornadofx:1.3.2' 'no.tornado:tornadofx-controls:1.0' 'no.tornado:fxlauncher:1.0.6')

Setting an Icon for All Views and Fragments

Since TornadoFX manages all Views and Fragments, I wonder if there is an easy way to set a graphic icon for all of them. I don't think there is a direct way to do this with JavaFX.

myStage.icons += Image("icon.png") 

Make ApacheHttpClient an optional dependency

I propose adding an interface to the HTTP operations in the REST client, with a default implementation using HttpURLConnection and an optional implementation that uses Apache HttpClient.

The default version should at least support basic authentication without the need for HttpClient. The HttpClient dependencies in Rest should be removed.

Consider special support for JsonProperty<T>

I'm contemplating a special kind of Property<T> that knows how to convert itself to and from JSON, for even tighter integration with JsonModel. We could support a new property delegate called jsonProperty to make it easy to generate these.

No details yet, don't even know if it's a good idea, just creating the issue and will let it stew for a while.

Integrate CrashFX

This is more of a reminder to myself than a request, but last year I wrote a crash handler library for JavaFX:

https://github.com/vinumeris/crashfx

It came with a simple web collector/dashboarding thing, although it's very crude and not suitable for high volumes of crash reports.

It doesn't make sense to maintain this as a separate library. I think it'd be better to merge the code into TornadoFX to upgrade the built in crash dialog.

What do you think?

We need some more builders

I'll go ahead and take care of this one this week, but I notice a few type-safe builders might need to be added. I'll keep adding to this list as I continue using TornadoFX for a project this week.

  • scrollpane()
  • flowpane()
  • tilepane()
  • anchorpane()
  • listview()

Restructure Builders.kt and Nodes.kt into Controls.kt, ItemControls.kt and Layouts.kt

For a while I've been bothered by the structure of Builders.kt vs Nodes.kt. We have stuff in Builders.kt that are not builders, but rather just extension functions that probably belong in Nodes.kt. (Example: TableView<S>.column - this does not change the hierarchy, and is not an extension of Pane).

Maybe it's time to consider organising this differently. We could create one file for List/Table-like controls, and include both builders and extensions for these controls in that file. Similarly, all text based builders and extensions like TextField and TextArea could go into one file etc.

This will make it much easier to see what's available (and what's missing) for a certain control or group of controls.

I'm still very fond of keeping everything in the tornadofx package, so you still only need a single import to work with the whole framework.

This change would not affect usage of the framework in any way.

Thoughts or other suggestions?

Launching with a main() function?

I might sound like a complete amateur here. How exactly do I launch a TornadoFX App class with a main method? Whenever I try to configure IDEA to launch my MainApp class, it does not find my main() function.

class MainApp : App() {
    override val primaryView = HelloWorld::class

    init {
        //importStylesheet("/styles.css")
    }

    class HelloWorld : View() {
        override val root = HBox(Label("Hello world"))
    }

    //IDEA does not see this main() function
    fun main(args: Array<String>) {
        launch("")
    }
}

MenuItem- Make event handler lambda optional

I've been working on RxJavaFX to implement reactive streams for ActionEvent, including Menus. I think it might be helpful to make the event handler for menuitem optional, especially for us Rx folks to use Observable or EventStream.

So instead of having to write this with a placeholder { }

menuitem("Press Me") { }
      .actionEvents().map { buildNewList() }.subscribe { myTable.items = it }

I can do this instead.

menuitem("Press Me")
      .actionEvents().map { buildNewList() }.subscribe { myTable.items = it }

Integrate DI

This is probably more a discussion piece then an issue or request. And it might get long :) so please bear with me.
Disclaimer

javafx and desktop development is even after 30 years still quite new to me, I usually write large scale backends.

I am writing a javafx implementation of keepass. All communication is done using the Guava EventBus. This means that When you select a group (see image) an event is sent that is picked up by the tableview and so on. It works quite nice.
During the writing of this I stumbled on Kotlin, was interested, so now part of it is in Kotlin, part in javafx. I am using fx-guice. I was looking at rewriting the whole ui part in kotlin, but I am looking at a decent way to implement guice into it.

Is there going to be decent hook into DI (Guice or heaven forbid spring?)

screenshot from 2016-04-10 21-09-01

Needs LICENSE file?

This repository doesn't have a LICENSE file, so I'm not sure what the code base is licensed under. GPL? MIT?

TableView Builder?

Do you think it is possible to create a TableView builder? I think it would go nicely with the TableView extensions already present. I can put in a PR and try to work out the details if needed, unless somebody has a faster vision than me on how to do it.

tableview {
        items = getMyItems()
        addColumn<ReportItem,String>("Category") { it.value.name }
        addColumn<ReportItem,BigDecimal>("Week") { it.value.weekTotal }
}

Easy access to application resources

Lot's of JavaFX API's takes resources as an URL or the toExternalForm of an URL. To retrieve a resource url one would typically write something like:

val myAudioClip = AudioClip(MyView::class.java.getResource("/sounds/mysound.wav").toExternalForm())

I propose adding a resources object to Component which can retrieve the external form url of a resource like this:

val myAudiClip = AudioClip(resources["/sounds/mysound.wav"])

If you need an actual URL it can be retrieved like this:

val myResourceURL = resources.url("/sounds/mysound.wav")

Resources are relative to the Component so access to a resource in the same package as the component would simply be resources["mysound.wav"].

Name of Maven artifact

If the Java packager is used to deploy the JavaFX application the Jarfile artifact is named fx-1.2.2.jar. Could we rename the Maven artifact from fx to tornadofx for clearity?

Create a Maven Archetype for TornadoFX

The archetype should create a minimal sample application and set up all dependencies.

I don't have much Gradle experience, but if there is a similar construct, we should provide an archetype for Gradle as well.

Async loading of items for datadriven components

There is a common pattern of loading remote data in the background and applying the result to a data driven component like for example TableView. This is supported via the background and ui functions:

background { controller.listItems() } ui { table.items = it }

This is so common in fact, that data driven components should not have to do this explicitly. Instead, these components could have an extension function called asyncItems that handles this. Usage:

table.asyncItems { controller.listItems() }

Other data driven components like ListView and ComboBox would benefit from the same extension.

Issue with TableView and Bindings?

Hey is there an issue with using Binding for mapped properties in a TableView? I am using RxJavaFX and an extension method to turn an Observable into a Binding.

class MyClass {
    val description = attributes.map { it.desc }.toBinding()
}

When I declare a TableView<MyClass> and map to the Binding, I am getting the result of toString() on the Binding object.

column("Description",MyClass::description)

Fragment should not be injectable

It's possible to inject Fragment. This leads to unexpected behavior because every field access will yield a new instance of the fragment. I propose to add an interface Injectable to View and Controller and change the inject delegate to only accept injectable types.

Nicer way to access @FXML properties in View

To access @FXML injected properties in a View you have to use the lateinit keyword:

@FXML lateinit var name: TextField

This is a bit ugly, and the field is not immutable. It would be preferable to create an immutable field and get rid of the @FXML annotation as well:

val name: TextField by fxid()

The old syntax would ofcourse still work, since it's already provided by the default FXMLLoader.

Injection support in the `App` class

It would be convenient to be able to inject resources into the App class, for easier configuration of resources before the app is started. An example would be configuration of the Rest interface. Instead of using find(Rest::class) one could inject a rest member in the normal way and configure it in init.

More elegant builders/extension functions for GridPane?

Really liking the vision of this library so far. I'm kind of fond of using GridPane because it gives a lot of layout control. Although it doesn't add much boilderplate, I wonder if there is a better way than calling apply() on a Node and then a bunch of static GridPane manipulation methods (as shown in bold below).

class MainApp : App() {
    override val primaryView = MainView::class

    class MainView : View() {
        override val root = GridPane()

        init {
            with(root) {
                tabpane {
                    apply {
                        GridPane.setVgrow(this,Priority.ALWAYS)
                        GridPane.setHgrow(this,Priority.ALWAYS)
                    }
                    tab("Report", GridPane()) {
                        row {
                            this += ReportTable.create().apply {
                                GridPane.setHgrow(this, Priority.ALWAYS)
                                GridPane.setVgrow(this,Priority.ALWAYS)
                            }
                        }
                    }
                }
            }
        }
    }
}

Perhaps we can add extension methods to Node that relate to the GridPane? But then again that might be confusing to see an extended hGrow property on a Node because it is not obvious it relates to the GridPane.

Saving components to properties with builders?

I hope I don't sound too ignorant. I don't have much practical experience with MVC paradigms, although this library has gotten me very interested in them. I often avoided them in Swing because they felt like they added boilerplate.

But that aside, I am trying to think about how to save components to properties so I can create and combine RxJava streams off them outside the builders. Is this an acceptable pattern?

 class MyView: View() {
        override val root = VBox()

        var myButton: Button?

        init {
            with(root) {
                    myButton = button("New Entry")
            }
       }
       fun getEvents(): Observable<ActionEvent> =
            myButton?.toNodeEvents(ActionEvent.Action)
}

If so, would it add any value to create a SingleAssign property delegate so we can not only rid the null checking, but also ensure a value gets only assigned once?

class SingleAssign<T> {

    @Volatile
    private var initialized = false

    @Volatile
    private var _value: Any? = UNINITIALIZED_VALUE

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (!initialized)
            throw Exception("Value has not been assigned yet!")
        return _value as T
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        synchronized(this) {
            if (initialized) {
                throw Exception("Value has already been assigned!")
            }
            _value = value as T
            initialized = true
        }
    }


    private object UNINITIALIZED_VALUE
}

Then we could call it like this.

 class MyView: View() {
        override val root = VBox()

        var myButton: Button by SingleAssign()

        init {
            with(root) {
                    myButton = button("New Entry")
            }
       }
       fun getEvents(): Observable<ActionEvent> =
            myButton.toNodeEvents(ActionEvent.Action)
}

Tests for Builders.

To test the builder pattern functions we need to create Nodes, thus we need to start the JavaFX thread.

There are JfxRunner (used by mvvmFX), Automaton and TestFX to start the JavaFX thread.

I'd like to improve TestFX's hasChildren() matcher and test the functions in Builders.kt with it. A test could look like this:

@Test
fun `label()`() {
    // when:
    val parent = StackPane().apply { label("foo") }

    // then:
    assertThat(parent, hasChild("foo"))
}

Some More Control Builders

As I'm documenting the guide, I'm doing a sweep for more simple controls that probably should be added

ToggleButton
RadioButton
Slider
TextFlow
ChoiceBox
PasswordField
Separator
HyperLink
ColorPicker
FileChooser
Pagination
HTMLEditor

I know the TextFlow is a lesser-used control, but considering JavaFX lacks html formatting for label text (like Swing) it is helpful to format a string of text with different colors. I use this feature quite a bit to concatenate data with different conditional formatting rules.

Let me know if we want to scratch any of these, and I'll work on adding whatever we wan)

TableView Builder- function-driven columns?

Hey a piece of my code broke with the 1.3.2 release. Do we still support creating columns off functions? How would I do this now?

categories.subscribe { cat->
     column<ValuedTextFlow>(cat) { it.value.sumForCategory(cat) }
}

Some Selection Extension Properties should be nullable

Documenting another item that I will fix this weekend. Some of the selection extension properties should be nullable. For example, Nodes.kt has some controls with a selectedItem or selectedValue property that could be null.

val <T> TableView<T>.selectedItem: T
    get() = this.selectionModel.selectedItem

val <T> TreeView<T>.selectedValue: T
    get() = this.selectionModel.selectedItem.value

...

val <T> ListView<T>.selectedItem: T
    get() = selectionModel.selectedItem

val <T> ComboBox<T>.selectedItem: T
    get() = selectionModel.selectedItem

To avoid nullability issues, they should be this. I'll do a sweep later to ensure nullable extension properties are indeed nullable.

val <T> TableView<T?>.selectedItem: T?
    get() = this.selectionModel.selectedItem

val <T> TreeView<T?>.selectedValue: T?
    get() = this.selectionModel.selectedItem.value

...

val <T> ListView<T?>.selectedItem: T?
    get() = selectionModel.selectedItem

val <T> ComboBox<T/>.selectedItem: T?
    get() = selectionModel.selectedItem

Streamlining CellFactories

I wonder if there is opportunity to improve the cell factories for TableView and other data controls.

For instance, setting a mutable column in TableView to use a ComboBoxTableCell feels like it could be streamlined.

column("CATEGORY", ParsedTransaction::categoryProperty).apply {
    cellFactory = ComboBoxTableCell.forTableColumn(Category.all)
    setOnEditCommit {
        it.tableView.items[it.tablePosition.row].category = it.newValue
    }
}

It really should be just as easy as giving a List<R> or an ObservableList<R> for a given TableColumn<T,R> and the ComboBox just plugs in automagically.

column("CATEGORY", ParsedTransaction::categoryProperty).apply {
        applyComboBox(Category.all)
}

Delegates for JavaFX Properties

I wonder if we could simplify the creation of Property fields and related methods somehow. In Java the general structure of a read-write property is (raw version):

private Property<T> fooProperty;
public Property<T> fooProperty() { return fooProperty; }
public void setFoo(T value) { /* lazy create property object and set value */ }
public T getFoo() { /* lazy create property object and get value */ }

This could be expressed in Kotlin as:

private val fooProperty by lazy { SimpleObjectProperty<T>() }
fun fooProperty() = fooProperty
var foo: T
    get() = fooProperty.get()
    set(value) = fooProperty.set(value)

In #javafx on kotlinlang.slack.com I wrote an incomplete implementation using Kotlin's delegated properties to simply this code. It could look like this:

var foo by lazyProperty { SimpleObjectProperty<T>() }
fun fooProperty() = foo.internalProperty

(This is an excellent framework. Good work.)

Injection patterns for reactive models?

I have found the injection model to feel strangely backwards when used with reactive programming. I am using RxKotlinFX, which is backed by RxJava and RxJavaFX.

Here's a little theory that might be driving this, and forgive me if I'm wrong on any assumptions. I'm not an expert on MVC at all.

Traditional imperative models would have the View pull items from the Controller, which would lazily trigger instantiating the Controller. But a reactive model is likely to push items from the Controller to the View. It is an inversion of control.

So it is a little awkward to inject a Controller into a View, when really the View should be injected into the Controller. Otherwise, with the current injection pattern the View will actually be doing all the work and not the Controller.

If I'm not making any sense, try to think about migrating the reactive logic below from the View to the Controller. Unless I'm missing something, you might realize there is no way to accomplish this without a lot of messy initialization and anti-patterns to interop the two.

class MyApp: App() {
    override val primaryView = MyView::class
}

class MyView: View() {

    override val root = VBox()

    val controller: MyController by inject()

    var countLabel: Label by singleAssign()
    var button: Button by singleAssign()
    var itemsList: ListView<String> by singleAssign()

    init {
        with(root) {
            button = button("Increment")
            countLabel = label("")
            itemsList = listview()
        }

        //everything else below needs to move to the Controller

        //increment number on countLabel every time button is pressed
        button.actionEvents()
                .map { 1 }
                .startWith(0)
                .scan { x,y -> x + y }
                .map { it.toString() }
                .subscribe { countLabel.text = it }

        //subscribe items into itemsList
        controller.items.subscribe { itemsList.items.add(it) }
    }
}

class MyController: Controller() {
    val items = Observable.just("Alpha","Beta","Gamma","Delta","Epsilon")
}

CSS DSL

Just an idea: should there be a DSL for adding CSS? Either to a node or to the View.

TableView column resizer extension function

One thing that frustrated me for awhile is having a way to fit TableView columns to the header and cell contents. Then I found a little gem in a Stack Overflow post.

I was able to come up with this class to expose that resizeColumnToFitContent()

public final class TableColumnResizer<T> extends TableViewSkin<T> {

    public TableColumnResizer(TableView<T> tableView) {
        super(tableView);
    }

    @Override
    public void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
      super.resizeColumnToFitContent(tc,maxRows);
    }
}

Then I can apply that TableViewSkin to the skinProperty(), and call a resizeOp function at any time to re-fit the columns.

var resizeOp: (() -> Unit) by singleAssign()

//layout built here

tableView.apply {
    val resizerSkin = TableColumnResizer<FTRecord>(this);
    skinProperty().set(resizerSkin)
    resizeOp = { columns.forEach { resizerSkin.resizeColumnToFitContent(it, 100) } }
}

It would be awesome if we could apply a resizeColumns() extension function to the TableView. The problem is I can't think of an efficient way at the top of my head without extending TableView or doing some lazy operation that stores state elsewhere. It's really too bad extension properties can't be fields.

Provide convenience for filtered/sorted list data in TableView

Sorting data in a TableView basically requires you to juggle three different observable lists. The workflow is as follows:

  1. Create a list of the actual table data (tableData)
  2. Create a FilteredList that wraps tableData (filteredData)
  3. Create a SortedList that wraps the filteredData and binds its comparatorProperty() to the TableView's comparatorProperty()
  4. Update the filteredData.predicate when ever the filtering conditions change

It would be convenient to be able to express all this with a single statement.

i18n Support

TornadoFX should provide an easy way to work with multiple UI languages.

TreeTableView Builder

I'd like to explore streamlining the creation of TreeTableView with a builder in a similar manner as TableView. Not quite sure of the details yet but I imagine the nested structure nature would make it a a good candidate.

SingleViewApp for demos and small apps

More and more issues post code samples with a view. It would be nice if these code samples were runnable as an application, without the need for extending the App class and wrapping the View etc. I propose adding a SingleViewApp with is both a View and and App in one to make it easier to very write small standalone applications.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.