FX Experience Has Gone Read-Only

I've been maintaining FX Experience for a really long time now, and I love hearing from people who enjoy my weekly links roundup. One thing I've noticed recently is that maintaining two sites (FX Experience and JonathanGiles.net) takes more time than ideal, and splits the audience up. Therefore, FX Experience will become read-only for new blog posts, but weekly posts will continue to be published on JonathanGiles.net. If you follow @FXExperience on Twitter, I suggest you also follow @JonathanGiles. This is not the end - just a consolidation of my online presence to make my life a little easier!

tl;dr: Follow me on Twitter and check for the latest news on JonathanGiles.net.

In JavaFX 1.3 a lot of work has gone into ListView, and extracting out the base virtualization engine (the secret sauce to high performance lists) such that it is able to be used in controls such as TreeView and TableView. At the same time we wanted to make it really easy for developers to customize what is shown in each ListView row. What we’ve ended up doing is creating the concept of a cell, which at any point in time represents at most one item in your ListView, and it is through the cell that you have total freedom about how to display your data. Finally, as noted, we’ve expanded this concept to also be used in exactly the same way in TreeView (which is a preview in JavaFX 1.3), and it is likely to also underpin any future TableView control.

The ListView and TreeView controls support a huge number of items, but the ‘cost’ of having the control actually visually represented is only what you see on screen. Of course, there is also the cost of simply having the data lying around (which is not really in the domain of the control to handle – needless to say – be smart 🙂 ). In other words, we are very smart about not creating nodes for list items that aren’t actually visible, and about updating the cell content based on user interactions such as scrolling and keyboard navigation. Also, each cell is reused, rather than being created and thrown away as necessary for each individual item in the ListView.

Of course, the other part of our job is to make easy to use API’s, so we’ve worked hard on this aspect as well. By default when you put an item into a ListView, we create a simple String-based cell that shows the text in the cell when necessary. In other words, if all you’re wanting is the same output as what you saw in JavaFX 1.2, you don’t need to change anything. On the other hand, if you want a more fancy ListView, that is where the concept of a cell factory comes into play. You can see a cell factory on a ListView control, and then whenever the ListView needs to update (because the visible items change), the cellFactory is consulted and you’re responsible for populating the cell as appropriate.

Prior to showing off the code, it would be useful to quickly summarize our key design goals, so that you’ll hopefully better understand why we did things the way we did. Our goals were the following:

  • Both time and memory efficient for large data sets
  • Easy to build and use libraries for custom cells
  • Easy to customize cell visuals
  • Easy to customize display formatting (12.34 as $12.34 or 1234% etc)
  • Easy to extend for custom visuals
  • Easy to have “panels” of data for the visuals
  • Easy to animate the cell size or other properties

Right, now definitely seems like a good time to actually show off some code. As a first example, if I were to create a custom cell which formatted numbers such that they would appear as currency types, I might do so like this:

[jfx]
import javafx.scene.control.*;

ListView {
items: [1.23, 3.33, 4.83, 5.32, 6.32]
cellFactory: function() {
def cell:ListCell = ListCell {
node: Label {
text: bind if (cell.empty) then "" else "${%(,.2f cell.item as Number}"
}
}
}
}
[/jfx]

In this example the cellFactory is a simple function which returns a ListCell who’s node content is a Label. The text of that Label is bound to the cell’s item. Whenever the item associated with a Cell is changed (which might happen if the user scrolls and the cell is reused to represent some different item in the ListView), then the Label’s text is automatically updated. In addition, if the cell is “empty” (meaning it is used to fill out space in the ListView but doesn’t have any data associated with it), then we just use the empty String.

Cell factories are really very powerful. The custom cells that are created can be anything from simple strings to full blown panels with movies and any other arbitrary content. They can change their size dynamically, and they can be styled easily from CSS.

We make it extraordinarily simple in JavaFX to change the color of cells. Each cell can be styled directly from CSS. So for example, if you wanted to change the default background of cells in a ListView to be a bit more interesting you could do the following CSS:

.list-cell {
   -fx-padding: 3 3 3 3;
   -fx-background-color: honeydew;
}

If you run an application with this style, you will notice that not all cells use this new color, only half of them. The other half (as it turns out, the odd numbered rows) are styled by a different rule in CSS which also needs to be specified:

.list-cell:odd {
    -fx-background-color: mistyrose;
}

Yikes! Looks like a crazy pair of socks. Clearly Jasper was not involved in picking the colors for this posting 🙂

If you wanted to set the color of selected ListView cells to be yellow, you could add this to your CSS file (yikes, the result is truly fearsome!):

.list-cell:filled:selected:focused, .list-cell:filled:selected {
    -fx-background-color: yellow;
    -fx-text-fill: black;
}

Each of these examples require no code changes. Simply update your CSS file to alter the colors. You can also use the “hover” and other pseudoclasses in CSS the same as with other controls.

Suppose you had a sequence of Numbers to display in a ListView and wanted to color all of the negative values red and all positive or 0 values black. One way to achieve this is with a custom cellFactory which changes the styleClass of the Cell based on whether the value is negative or positive:

[jfx]
import javafx.scene.control.*;

ListView {
items: [1.23, -3.33, -4.83, 5.32, -6.32]
cellFactory: function() {
def cell:ListCell = ListCell {
styleClass: bind if ((cell.item as Number) < 0) then "negative" else "list-cell"
node: Label { text: bind if (cell.empty) then "" else "{cell.item}" }
}
}
}
[/jfx]

You would then update the CSS file so that the text-fill of anything with a styleClass equal to “negative” would use red:

.negative {
    -fx-text-fill: red
}

There is one problem with this solution in JavaFX 1.3 however, which is that we currently only support a single style class, whereas in HTML CSS multiple style classes are supported. We’re working hard on fixing this limitation. In the meantime, you will need to copy a chunk of the default caspian stylesheet for your “negative” class for this to work properly:

.negative {
    -fx-text-fill: red;
    -fx-cell-size: 24;
    -fx-padding: 3;
    -fx-background-color: -fx-control-inner-background;
}

.negative:focused {
    -fx-background-insets: 0, 1.4;
    -fx-background-color: -fx-focus-color, -fx-control-inner-background;
}

.negative:odd {
    -fx-background-color: derive(-fx-control-inner-background,-5%);
}

.negative:focused:odd {
    -fx-background-insets: 0, 1.4;
    -fx-background-color: -fx-focus-color, derive(-fx-control-inner-background,-5%);
}

.negative:filled:selected:focused, .negative:filled:selected {
    -fx-background: -fx-accent;
    -fx-background-color: -fx-selection-bar;
    -fx-text-fill: -fx-selection-bar-text;

}

Cell Factories
Although the code necessary for creating cell factories is relatively terse, it is also repetitive boilerplate that could be (and should be) handled by libraries. For example, you could provide static functions which create various standard ListCell factories:

[jfx]
import javafx.scene.control.*;

function currencyCellFactory():ListCell {
def cell:ListCell = ListCell {
node: Label { text: bind if (cell.empty) then "" else "${%(,.2f cell.item as Number}" }
}
}

ListView {
items: [1.23, 5.23, 4.44, 5.93, 1.34, 33.23]
cellFactory: currencyCellFactory
}
[/jfx]

Or the following example might be used to generate a cell factory which can format dates according to some specified format String:

[jfx]
import javafx.scene.control.*;

function dateCellFactory(format:String):ListCell {
// … exercise left to the reader
ListCell { }
}

ListView {
items: [date1, date2, date3, date4]
cellFactory: dateCellFactory("%tF")
}
[/jfx]

There are times when you may wish to create much more elaborate factories based on many different parameters. One approach is to create a class which represents the state of the factory, and which has one or more factory functions on it. For example, I might write the following class:

[jfx]
public class MyCellFactory {
public var format:String;
public function listCellFactory():ListCell {

}
public function treeCellFactory():TreeCell {

}
}
[/jfx]

And then when creating my ListView or TreeView, I could instantiate an instance of MyCellFactory and then link to the appropriate cellFactory function defined on that class:

[jfx]
ListView {
items: [1.23, 3.24, 5.32]
cellFactory: MyCellFactory { format: "%(,.2f" }.listCellFactory
}
[/jfx]

In this way the parameters, functions, state, and so forth associated with the custom cell factories can be grouped together in a sensible way.

Building a panel of data
Since the content node of a Cell may be any node type, it is possible to build up more complicated Cell content than a single text string. For example, suppose you wanted to produce a ListView that was composed of a Label and a Button. Suppose each of these cells represents a Job object in your application’s domain model. Perhaps clicking the button is supposed to start a Job. You might build the cell like this:

[jfx]
ListView {
items: bind jobs
cellFactory: function() {
def cell:ListCell = ListCell {
node: HBox {
spacing: 7
content: [
Label { text: bind if (cell.empty) then "" else "{(cell.item as Job).title}" }
Button {
text: "Start"
visible: bind cell.selected and not cell.empty
action: function() {
def job = cell.item as Job;
job.start();
}
}
]
}
}
}
}
[/jfx]

When the Cell is selected, the visible attribute of the Button changes to true and you can see the button. Click on the button and it will start the Job. You can create arbitrarily complicated Cells in this manner.

These are just a few of the many interesting things that can be done with the ListView control in JavaFX 1.3. We’ll have additional blog posts in the near future showing in more detail some specific tricks (such as how to make a cell change size dynamically, and some more Jasper-approved visual design tweaks to make ListView really stand out).

In the meantime, here’s a call to action for all you experts out there doing library work (hint hint, Stephen and Dean). I’m excited to see what kind of cool libraries of cell factories can be built to make the ListView control really sweet to use!