-
Notifications
You must be signed in to change notification settings - Fork 272
Type Safe Builders
Wiki ▸ Documentation ▸ 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
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")
}
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
}
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.
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