Skip to content
This repository has been archived by the owner on Sep 28, 2024. It is now read-only.

Type Safe Builders

Edvin Syse edited this page Sep 23, 2016 · 31 revisions

WikiDocumentationType Safe Builders

Type Safe Builders

Builders are extension functions to the Java FX Pane class which enables you to create a new node, set some properties and add it to the children of the parent Pane with very little code. The hierarchical nature of the builders makes it easy to understand the ui composition with a simple glance.

There are builders for complex Node components as well, like TableView.

Builders also support automatic property binding for input type components like TextField, ComboBox etc.

The full list of available functions can be seen in the sources, mainly in ItemControls.kt, Controls.kt and Layouts.kt. If you miss a feature, send us a pull request, and we'll be happy to include it.

###Example 1 ####A VBox containing two HBox components

Each HBox contains a Label and a TextField, both which utilize a margin constraint within the HBox. The TextField components are also set to use the maximum allowable width.

class MyView : View() {

    override val root = VBox()

    init {
        with(root) {
            hbox {
                label("First Name") {
                    hboxConstraints { margin = Insets(5.0) }
                }
                textfield {
                    hboxConstraints { margin = Insets(5.0) }
                    useMaxWidth = true
                }
            }
            hbox {
                label("Last Name") {
                    hboxConstraints { margin = Insets(5.0) }
                }
                textfield {
                    hboxConstraints { margin = Insets(5.0) }
                    useMaxWidth = true
                }
            }
        }
    }
}

Rendered UI

Note also you can add Node instances to a builder without using their builder equivalents This is helpful if you have Node components existing outside your builder or are working with Node items that have no builder support. You can then call the Kotlin stdlib function apply() to then maintain the "builder flow".

class MyView : View() {

    override val root = VBox()

    init {
        with(root) {
            hbox {
                this += Label("First Name").apply {
                    hboxConstraints { margin = Insets(5.0) }
                }
                this += TextField().apply {
                    hboxConstraints { margin = Insets(5.0) }
                    useMaxWidth = true
                }
            }
            hbox {
                this += Label("Last Name").apply {
                    hboxConstraints { margin = Insets(5.0) }
                }
                this += TextField().apply {
                    hboxConstraints { margin = Insets(5.0) }
                    useMaxWidth = true
                }
            }
        }
    }
}

###Example 2

A TableView

You can build an entire TableView, complete with a backing list and column value mappings, with a succinct builder structure.

Say you have the given domain object Person.

class Person(id: Int, name: String, birthday: LocalDate) {
    var id by property<Int>()
    fun idProperty() = getProperty(Person::id)

    var name by property<String>()
    fun nameProperty() = getProperty(Person::name)

    var birthday by property<LocalDate>()
    fun birthdayProperty() = getProperty(Person::birthday)

    //assume today is 2016-02-28
    val age: Int get() = Period.between(birthday, LocalDate.now()).years

    init {
        this.id = id
        this.name = name
        this.birthday = birthday
    }
}

You can easily declare a TableView using a builder.

class MyView : View() {

    override val root = VBox()

    private val persons = FXCollections.observableArrayList<Person>(
            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))
    )

    init {
        with(root) {
            tableview(persons) {
                column("ID", Person::idProperty)
                column("Name", Person::nameProperty)
                column("Birthday", Person::birthdayProperty)
                column("Age", Person::ageProperty)
            }
        }
    }
}

RENDERED UI

Note that the four data properties are in fact JavaFX Properties while the age property is not. The TableView builder is smart enough to work with either pattern and handle that abstraction, even accounting for both getters and setters.

You can also specify custom cell formatters quickly by calling cellFormat() on a TableColumn. For example, we can highlight cells in red below where the Age is less than 18.

class MyView : View() {

    override val root = VBox()

    private val persons = FXCollections.observableArrayList<Person>(
            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))
    )

    init {
        with(root) {
            tableview(persons) {
                column("ID",Person::idProperty)
                column("Name", Person::nameProperty)
                column("Birthday", Person::birthdayProperty)
                column("Age",Person::ageProperty).cellFormat {
                    if (it < 18) {
                        style = "-fx-background-color:#8b0000; -fx-text-fill:white"
                        text = it.toString()
                    } else {
                        text = it.toString()
                    }
                }
            }
        }
    }
}

RENDERED UI

###Example 3 ####A TabPane with tabs containing GridPane layouts as well as a TableView

There are several builders for layouts including GridPane, BorderPane, VBox, and HBox. There are even builders to set up a TabePane with multiple Tab items, each containing a specified Pane.

This shows how easily we can compose complex layouts with minimal code.

class MyView : View() {

    override val root = GridPane()

    private val persons = FXCollections.observableArrayList<Person>(
            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))
    )

    init {
        with(root) {
            tabpane {
                gridpaneConstraints {
                    vhGrow = Priority.ALWAYS
                }
                tab("Report", HBox()) {
                    label("Report goes here")
                }
                tab("Data", GridPane()) {
                    tableview<Person> {
                        items = persons
                        column("ID", Person::idProperty)
                        column("Name", Person::nameProperty)
                        column("Birthday", Person::birthdayProperty)
                        column("Age", Person::ageProperty).cellFormat {
                            if (it < 18) {
                                style = "-fx-background-color:#8b0000; -fx-text-fill:white"
                                text = it.toString()
                            } else {
                                text = it.toString()
                            }
                        }
                    }
                }
            }
        }
    }
}

RENDERED UI

###Example 4 ####Menu Builders

You can also use the type-safe builders to create menus, including menus inside MenuBar or ContextMenu nodes.

class MenuView: View() {

    override val root = VBox()

    init {
        with(root) {
            menubar {
                menu("File") {
                    menu("Switch Account") {
                        menuItem("Facebook") { println("Switching to Facebook") }
                        menuItem("Twitter") { println("Switching to Twitter") }
                    }
                    separator()
                    menuItem("Save") { println("Saving") }
                    menuItem("Exit") { println("Exiting")}
                }
                menu("Edit") {
                    menuItem("Copy") { println("Copying") }
                    menuItem("Paste") { println("Pasting") }
                    separator()
                    menu("Options") {
                        menuItem("Account") { println("Launching Account Options") }
                        menuItem("Security") { println("Launching Security Options") }
                        menuItem("Appearance") { println("Launching Appearance Options") }
                    }
                }
            }
        }
    }
}

RENDERED UI

You can also specify accelerator and/or graphic node arguments for each menuitem.

menuitem("Facebook", Icons.Facebook, KeyCombination.valueOf("Shortcut+F")) {
    println("Switching to Facebook")
}

TreeView

Let's create a hierarchical view of persons by the department they work in.

val persons = listOf(
        Person("Mary Hanes","Marketing"),
        Person("Steve Folley","Customer Service"),
        Person("John Ramsy","IT Help Desk"),
        Person("Erlick Foyes","Customer Service"),
        Person("Erin James","Marketing"),
        Person("Jacob Mays","IT Help Desk"),
        Person("Larry Cable","Customer Service")
)

// Create Person objects for the departments with the department name as Person.name
val departments = persons.map { it.department }.distinct().map { Person(it, "") }

val view = treeview<Person> {
    // Create root item
    root = TreeItem(Person("Departments", ""))

    // Make sure the text in each TreeItem is the name of the Person
    cellFormat { text = it.name }

    // Generate items. Children of the root item will contain departments
    populate { parent ->
        if (parent == root) departments else persons.filter { it.department == parent.value.name }
    }
}

The populate function is called for each TreeItem. If the TreeItem returns any children, the populate function will again be called for each child. This way, you can keep recursing as deep as you want.

We first check if the parent sent in is the root node. This is how we know to just return the list of departments. For the next iteration the parent will be a TreeItem holding a department. Simply filter out all Person objects that has the same department. This is where it gets ugly when you repurpose the same domain object for both Department and Person. To solve this, one should create a common data class, or simply type the TreeView as TreeView<*> and create a data class for Department as well:

data class Department(val name: String)

// Create Department objects for the departments by getting distinct values from Person.department
val departments = persons.map { it.department }.distinct().map { Department(it) }

// Type safe way of extracting the correct TreeItem text
cellFormat {
    text = when (it) {
        is String -> it
        is Department -> it.name
        is Person -> it.name
        else -> throw IllegalArgumentException("Invalid value type")
    }
}

// Generate items. Children of the root item will contain departments, children of departments are filtered
populate { parent ->
    val value = parent.value
    if (parent == root) departments
    else if (value is Department) persons.filter { it.department == value.name }
    else null
}

TreeTableView

Let's sort leaders by their employees in a TreeTableView with three columns.

class Person(val name: String, val department: String, val email: String, val employees: List<Person> = emptyList())

val persons = FXCollections.observableArrayList(
        Person("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", listOf(
            Person("Jacob Mays", "IT Help Desk", "jacob.mays@contoso.com"),
            Person("John Ramsy", "IT Help Desk", "john.ramsy@contoso.com"))),
        Person("Erin James", "Human Resources", "erin.james@contoso.com", listOf(
            Person("Erlick Foyes", "Customer Service", "erlick.foyes@contoso.com"),
            Person("Steve Folley", "Customer Service", "steve.folley@contoso.com"),
            Person("Larry Cable", "Customer Service", "larry.cable@contoso.com")))
)

val tableview = TreeTableView<Person>().apply {
    column("Name", Person::nameProperty)
    column("Department", Person::departmentProperty)
    column("Email", Person::emailProperty)

    /// Create the root item that holds all top level employees
    root = TreeItem(Person("Employees by leader", "", "", persons))

    // Always return employees under the current person
    populate { it.value.employees }

    // Expand the two first levels
    root.isExpanded = true
    root.children.forEach { it.isExpanded = true }

    // Resize to display all elements on the first two levels
    resizeColumnsToFitContent()
}

The populate function merely returns the employees of the Person that was passed in.

What if there is no builder for the control I'm adding?

Some times you'll run across nodes that don't fit well with the builder concept. Builders work on subclasses of Pane, so unless the framework has special support for the node you're adding, you have to take a slightly different approach when adding children to the node.

Consider the ButtonBar. It is a Control, but not a Pane, but it can contain child nodes in it's buttons property. To work around this, TornadoFX let's you specify the current builder scope, so you can choose where the generated children will be added. Let's first assume that TornadoFX did not have any special support for ButtonBar. We would have to work around this in the following manner:

hbox {
    // Add a component without using a builder
    this += ButtonBar().apply {
        // Set some properties on the ButtonBar
        prefHeight = 20.0
        prefWidth = 410.0

        // Add any children inside the lambda to the button list of the ButtonBar
        children(buttons) {
            button("Create").setOnAction { createDocument() }
            button("Cancel").setOnAction { close() }
        }
    }
}

Fortunately, TornadoFX supports buttons in the ButtonBar, so the above example can also be written like:

hbox {
    buttonbar {
        prefHeight = 20.0
        prefWidth = 410.0

        button("Create").setOnAction { createDocument() }
        button("Cancel").setOnAction { close() }
    }
}

If you add any other children to a ButtonBar or a ToolBar, you would wrap their builder statements in children(buttons) or children(items) respectively. Next: Forms

Clone this wiki locally