Type-Safe CSS

While you can create plain text CSS style sheets in JavaFX, TornadoFX provides the option to bring type-safety and compiled CSS to JavaFX. You can conveniently choose to create styles in its own class, or do it inline within a control declaration.

Inline CSS

The quickest and easiest way to style a control on the fly is to call a given Node's inline style { } function. All the CSS properties available on a given control are available in a type-safe manner, with compilation checks and auto-completion.

For example, you can style the borders on a Button (using the box() function), bold its font, and rotate it (Figure 6.1).

button("Press Me") {
    style {
        fontWeight = FontWeight.EXTRA_BOLD
        borderColor += box(
                top = Color.RED,
                right = Color.DARKGREEN,
                left = Color.ORANGE,
                bottom = Color.PURPLE
        )
        rotate = 45.deg
    }

    setOnAction { println("You pressed the button") }
}

Figure 6.1

This is especially helpful when you want to style a control without breaking the declaration flow of the Button. However, keep in mind the style { } will replace all styles applied to that control unless you pass true for its optional append argument.

style(append = true) {
      ....
}

Some times you want to apply the same styles to many nodes in one go. The style { } function can also be applied to any Iterable that contains Nodes:

vbox {
    label("First")
    label("Second")
    label("Third")
    children.style {
        fontWeight = FontWeight.BOLD
    }
}

The fontWeight style is applied to all children of the vbox, in essence all the labels we added.

When your styling complexity passes a certain threshold, you may want to consider using Stylesheets which we will cover next.

Applying Style Classes with Stylesheets

If you want to organize, re-use, combine, and override styles you need to leverage a Stylesheet. Traditionally in JavaFX, a stylesheet is defined in a plain CSS text file included in the project. However, TornadoFX allows creating stylesheets with pure Kotlin code. This has the benefits of compilation checks, auto-completion, and other perks that come with statically typed code.

To declare a Stylesheet, extend it onto your own class to hold your customized styles.

import tornadofx.*

class MyStyle: Stylesheet() {
}

Next, you will want to specify its companion object to hold class-level properties that can easily be retrieved. Declare a new cssclass()-delegated property called tackyButton, and define four colors we will use for its borders.

import javafx.scene.paint.Color
import tornadofx.*

class MyStyle: Stylesheet() {

    companion object {
        val tackyButton by cssclass()

        private val topColor = Color.RED
        private val rightColor = Color.DARKGREEN
        private val leftColor = Color.ORANGE
        private val bottomColor = Color.PURPLE
    }
}

Note also you can use the c() function to build colors quickly using RGB values or color Strings.

  private val topColor = c("#FF0000")
  private val rightColor = c("#006400")
  private val leftColor = c("#FFA500")
  private val bottomColor = c("#800080")

Finally, declare an init() block to apply styling to the classes. Define your selection and provide a block that manipulates its various properties. (For compound selections, call the s() function, which is an alias for the select() function). Set rotate to 10 degrees, define the borderColor using the four colors and the box() function, make the font family "Comic Sans MS", and increase the fontSize to 20 pixels. Note that there are extension properties for Number types to quickly yield the value in that unit, such as 10.deg for 10 degrees and 20.px for 20 pixels.

import javafx.scene.paint.Color
import tornadofx.*

class MyStyle: Stylesheet() {

    companion object {
        val tackyButton by cssclass()

        private val topColor = Color.RED
        private val rightColor = Color.DARKGREEN
        private val leftColor = Color.ORANGE
        private val bottomColor = Color.PURPLE
    }

    init {
        tackyButton {
            rotate = 10.deg
            borderColor += box(topColor,rightColor,bottomColor,leftColor)
            fontFamily = "Comic Sans MS"
            fontSize = 20.px
        }
    }
}

Now you can apply the tackyButton style to buttons, labels, and other controls that support these properties. While this styling can work with other controls like labels, we are going to target buttons in this example.

First, load the MyStyle stylesheet into your application by including it as contructor parameter.

class MyApp: App(MyView::class, MyStyle::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}

The reloadStylesheetsOnFocus() function call will instruct TornadoFX to reload the Stylesheets every time the Stage gets focus. You can also pass the --live-stylesheets argument to the application to accomplish this.

Important: For the reload to work, you must be running the JVM in debug mode and you must instruct your IDE to recompile before you switch back to your app. Without these steps, nothing will happen. This also applies to reloadViewsOnFocus() which is similar, but reloads the whole view instead of just the stylesheet. This way, you can evolve your UI very rapidly in a "code change, compile, refresh" manner.

You can apply styles directly to a control by calling its addClass() function. Provide the MyStyle.tackyButton style to two buttons (Figure 6.2).

class MyView: View() {
    override val root = vbox {
        button("Press Me") {
            addClass(MyStyle.tackyButton)
        }
        button("Press Me Too") {
            addClass(MyStyle.tackyButton)
        }
    }
}

Figure 6.2

Intellij IDEA can perform a quickfix to import member variables, allowing addClass(MyStyle.tackyButton) to be shortened to addClass(tackyButton) if you prefer.

You can use removeClass() to remove the specified style as well.

Targeting Styles to a Type

One of the benefits of using pure Kotlin is you can tightly manipulate UI control behavior and conditions using Kotlin code. For example, you can apply the style to any Button by iterating through a control's children, filtering for only children that are Buttons, and applying the addClass() to them.

class MyView: View() {
    override val root = vbox {
        button("Press Me")
        button("Press Me Too")

        children.asSequence()
                .filter { it is Button }
                .forEach { it.addClass(MyStyle.tackyButton) }
    }
}

Infact, manipulating classes on several nodes at once is so common that TornadoFX provides a shortcut for it:

children.filter { it is Button }.addClass(MyStyle.tackyButton) }

You can also target all Button instances in your application by selecting and modifying the button in the Stylesheet. This will apply the style to all Buttons.

import javafx.scene.paint.Color
import tornadofx.*

class MyStyle: Stylesheet() {

    companion object {
        val tackyButton by cssclass()

        private val topColor = Color.RED
        private val rightColor = Color.DARKGREEN
        private val leftColor = Color.ORANGE
        private val bottomColor = Color.PURPLE
    }

    init {
        button {
            rotate = 10.deg
            borderColor += box(topColor,rightColor,leftColor,bottomColor)
            fontFamily = "Comic Sans MS"
            fontSize = 20.px
        }
    }
}
import javafx.scene.layout.VBox
import tornadofx.*

class MyApp: App(MyView::class, MyStyle::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}
class MyView: View() {
    override val root = vbox {
        button("Press Me")
        button("Press Me Too")
    }
}

Figure 6.3

Note also you can select multiple classes and control types to mix-and-match styles. For example, you can set the font size of labels and buttons to 20 pixels, and create tacky borders and fonts only for buttons (Figure 6.4).

class MyStyle: Stylesheet() {

    companion object {

        private val topColor = Color.RED
        private val rightColor = Color.DARKGREEN
        private val leftColor = Color.ORANGE
        private val bottomColor = Color.PURPLE
    }

    init {
        s(button, label) {
            fontSize = 20.px
        }
        button {
            rotate = 10.deg
            borderColor += box(topColor,rightColor,leftColor,bottomColor)
            fontFamily = "Comic Sans MS"
        }
    }
}
class MyApp: App(MyView::class, MyStyle::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}

class MyView: View() {
    override val root = vbox {
        label("Lorem Ipsum")
        button("Press Me")
        button("Press Me Too")
    }
}

Figure 6.4

Other types of selectors

TornadoFX supports selecting nodes based on their element name, ID, and style-class, and also has support for pseudo-classes. The following example shows how each of them can be used:

class MyStyle: Stylesheet() {
    companion object {
        val udp by csselement("UpsideDownPane")
        val phantomZone by cssid()
        val fancyPants by cssclass()
        val interactive by csspseudoclass()
    }

    init {
        udp {
            fontSize = 20.px
        }
        phantomZone {
            backgroundColor += c("black")
        }
        fancyPants {
            backgroundColor += c("orange")
        }
        label and interactive {
            textFill = c("green")
        }
    }
}

And would result in the following CSS:

UpsideDownPane {
    -fx-font-size: 20px;
}
#phantom-zone {
    -fx-background-color: #000000ff;
}
.fancy-pants {
    -fx-background-color: #ff8000ff;
}
label:interactive {
    -fx-test-fill: #008000ff;
}

Multi-Value CSS Properties

Some CSS properties accept multiple values, and TornadoFX Stylesheets can streamline this with the multi() function. This allows you to specify multiple values via a varargs parameter and let TornadoFX take care of the rest. For instance, you can nest multiple background colors and insets into a control (Figure 6.5).

label("Lore Ipsum") {
    style {
        fontSize = 30.px
        backgroundColor = multi(Color.RED, Color.BLUE, Color.YELLOW)
        backgroundInsets = multi(box(4.px), box(8.px), box(12.px))
    }
}

Figure 6.5

The multi() function should work wherever multiple values are accepted. If you want to only assign a single value to a property that accepts multiple values, you will need to use the plusAssign() operator to add it (Figure 6.6).

label("Lore Ipsum") {
    style {
        fontSize = 30.px
        backgroundColor += Color.RED
        backgroundInsets += box(4.px)
    }
}

Figure 6.6

Nesting Styles

Inside a selector block you can apply further styles targeting child controls.

For instance, define a CSS class called critical. Make it put an orange border around any control it is applied to, and pad it by 5 pixels.

class MyStyle: Stylesheet() {

    companion object {
        val critical by cssclass()
    }

    init {
        critical {
            borderColor += box(Color.ORANGE)
            padding = box(5.px)
        }
    }
}

But suppose when we applied critical to any control, such as an HBox, we want it to add additional stylings to buttons inside that control. Nesting another selection will do the trick.

class MyStyle: Stylesheet() {
    companion object {
        val critical by cssclass()
    }
    init {
        critical {
            borderColor += box(Color.ORANGE)
            padding = box(5.px)
            button {
                backgroundColor += Color.RED
                textFill = Color.WHITE
            }
        }
    }
}

Now when you apply critical to say, an HBox, all buttons inside that HBox will get that defined style for button (Figure 6.7)

class MyApp: App(MyView::class, MyStyle::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}

class MyView: View() {
    override val root = hbox {
        addClass(MyStyle.critical)
        button("Warning!")
        button("Danger!")
    }
}

Figure 6.7

There is one critical thing to not confuse here. The orange border is only applied to the HBox since the critical class was applied to it. The buttons do not get an orange border because they are children to the HBox. While their style is defined by critical, they do not inherit the styles of their parent, only those defined for button.

If you want the buttons to get an orange border too, you need to apply the critical class directly to them. You will want to use the and() to apply specific styles to buttons that are also declared as critical.

class MyStyle: Stylesheet() {

    companion object {
        val critical by cssclass()
    }

    init {
        critical {

            borderColor += box(Color.ORANGE)
            padding = box(5.px)

            and(button) {
                backgroundColor += Color.RED
                textFill = Color.WHITE
            }
        }
    }
}
class MyApp: App(MyView::class, MyStyle::class) {
    init {
        reloadStylesheetsOnFocus()
    }
}

class MyView: View() {
    override val root = hbox {
        addClass(MyStyle.critical)

        button("Warning!") {
            addClass(MyStyle.critical)
        }

        button("Danger!") {
            addClass(MyStyle.critical)
        }
    }
}

Figure 6.8

Now you have orange borders around the HBox as well as the buttons. When nesting styles, keep in mind that wrapping the selection with and() will cascade styles to children controls or classes.

Mixins

There are times you may want to reuse a set of stylings and apply them to several controls and selectors. This prevents you from having to redundantly define the same properties and values. For instance, if you want to create a set of styling called redAllTheThings, you could define it as a mixin as shown below. Then you can reuse it for a redStyle class, as well as a textInput, a label, and a passwordField with additional style modifications (Figure 6.9).

Stylesheet

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

class Styles : Stylesheet() {

    companion object {
        val redStyle by cssclass().
    }

    init {
        val redAllTheThings = mixin {
            backgroundInsets += box(5.px)
            borderColor += box(Color.RED)
            textFill = Color.RED
        }

        redStyle {
            +redAllTheThings
        }

        s(textInput, label) {
            +redAllTheThings
            fontWeight = FontWeight.BOLD
        }

        passwordField {
            +redAllTheThings
            backgroundColor += Color.YELLOW
        }
    }
}

App and View

class MyApp: App(MyView::class, Styles::class)

class MyView : View("My View") {
    override val root = vbox {
        label("Enter your login")
        form {
            fieldset{
                field("Username") {
                    textfield()
                }
                field("Password") {
                    passwordfield()
                }
            }
        }
        button("Go!") {
            addClass(Styles.redStyle)
        }
    }
}

Figure 6.9

The stylesheet is applied to the application by adding it as a constructor parameter to the App class. This is a vararg parameter, so you can send in a comma separated list of multiple stylesheets. If you want to load stylesheets dynamically based on some condition, you can call importStylesheet(Styles::class) from anywhere. Any UIComponent opened after the call to importStylesheet will get the stylesheet applied. You can also load normal text based css stylesheets with this function:

importStylesheet("/mystyles.css")

Loading a text based css stylesheet

If you find you are repeating yourself setting the same CSS properties to the same values, you might want to consider using mixins and reusing them wherever they are needed in a Stylesheet.

Modifier Selections

TornadoFX also supports modifier selections by leveraging and() functions within a selection. The most common case this is handy is styling for "selected" and cursor "hover" contexts for a control.

If you wanted to create a UI that will make any Button red when it is hovered over, and any selected Cell in data controls such as ListView red, you can define a Stylesheet like this (Figure 6.10).

Stylesheet

import javafx.scene.paint.Color
import tornadofx.Stylesheet

class Styles : Stylesheet() {

    init {
        button {
            and(hover) {
                backgroundColor += Color.RED
            }
        }
        cell {
            and(selected) {
                backgroundColor += Color.RED
            }
        }
    }
}

App and View

import tornadofx.*

class MyApp: App(MyView::class, Styles::class)

class MyView : View("My View") {

    val listItems = listOf("Alpha","Beta","Gamma").observable()
and
    override val root = vbox {
        button("Hover over me")
        listview(listItems)
    }
}

Figure 6.10 - A cell is selected and the Button is being hovered over. Both are now red.

Whenever you need modifiers, use the select() function to make those contextual style modifications.

Control-Specific Stylesheets

If you decide to create your own controls (often by extending an existing control, like Button), JavaFX allows you to pair a stylesheet with it. In this situation, it is advantageous to load this Stylesheet only when this control is loaded. For instance, if you have a DangerButton class that extends Button, you might consider creating a Stylesheet specifically for that DangerButton. To allow JavaFX to load it, you need to override the getUserAgentStyleSheet() function as shown below. This will convert your type-safe Stylesheet into plain text CSS that JavaFX natively understands.

class DangerButton : Button("Danger!") {
    init {
        addClass(DangerButtonStyles.dangerButton)
    }
    override fun getUserAgentStylesheet() = DangerButtonStyles().base64URL.toExternalForm()
}

class DangerButtonStyles : Stylesheet() {
    companion object {
        val dangerButton by cssclass()
    }

    init {
        dangerButton {
            backgroundInsets += box(0.px)
            fontWeight = FontWeight.BOLD
            fontSize = 20.px
            padding = box(10.px)
        }
    }
}

The DangerButtonStyles().base64URL.toExternalForm() expression creates an instance of the DangerButtonStyles, and turns it into a URL containing the entire stylesheet that JavaFX can consume.

Conclusion

TornadoFX does a great job executing a brilliant concept to make CSS type-safe, and it further demonstrates the power of Kotlin DSL's. Configuration through static text files is slow to express with, but type-safe CSS makes it fluent and quick especially with IDE auto-completion. Even if you are pragmatic about UI's and feel styling is superfluous, there will be times you need to leverage conditional formatting and highlighting so rules "pop out" in a UI. At minimum, get comfortable using the inline style { } block so you can quickly access styling properties that cannot be accessed any other way (such as TextWeight).

results matching ""

    No results matching ""