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.

One of the things that would be really nice to have in the virtualised ListView and TreeView JavaFX controls, not to mention future controls like TableView, is multiple selection. Certain kinds of apps just can not exist without multiple selection in fact.

So, unsurprisingly, today I got an email from a user of JavaFX, who claims to be a fan of FX Experience (hi Keith!), who was needing multiple selection for his work. I didn’t actually think it could be done very easily (and one of my main jobs is working on these controls, so I should know), but I spent a bit of time looking into it, and it turns out that it’s actually quite possible, with a number of warnings and rough edges, and also the use of a little bit of unpublished API. As long as you’re promising to not tell anyone, I thought I’d share this code….but just with you, so shhh 🙂

What I present to you today therefore is to be considered proof of concept only. Using this in your day job is considered risky, as some of the code may not work in future releases. Being a proof of concept, the code below is not fully implemented (which I’ll detail more shortly), and performance may degrade somewhat as the number of selected items increases. No attempt at performance optimisation has been made. Proceed at your own risk 🙂 I hope that I’ve sufficiently scared you.

I wanted to briefly show the code, and then just provide you with a jar file to download and use in your own projects. Firstly though, here’s a screenshot I sent to Keith to detail what he should expect to see when he runs the new control:

listview-multiselect

Do not adjust your TV sets (for those of you, ah, accessing the net via your TV?). The ListView on the right looks odd because the label is rotated inside the cell, to make it apparent that you can get multiple selection even when using a custom cell factory. To get the multiple rows selected I just held down ctrl or shift as I left-clicked, as is usually the case in list controls in other UI toolkits. Here’s the code I wrote to create the app in the screenshot above:

[jfx]
Stage {
title: "Multiple Selection Demo"
scene: Scene {
width: 340
height: 260
content: [
HBox {
spacing: 10
content: [
MultiSelectListView {
items: [1..100]
},

MultiSelectListView {
items: [1..100]
cellFactory: function() {
def cell:ListCell = MultiSelectListCell {
node: Label {
rotate: 180
text: bind if (cell.item == null)
then "" else "{cell.item}";
}
}
}
}
]
}
]
}
}
[/jfx]

Pretty much the kind of code you’d expect to see when creating a ListView, except the control is called MultiSelectListView, and the ListCell used in the second list is called MultiSelectListCell.

The code for MultiSelectListView is shown below. There is nothing special in here, it simply extends ListView, and adds a bit more API for selectedIndexes and selectedItems, as well as a default cell factory if one isn’t specified by the user.

[jfx]
import javafx.scene.control.ListView;
import javafx.scene.control.ListCell;
import javafx.scene.Node;
import javafx.scene.control.Label;

public class MultiSelectListView extends ListView {

public-read var selectedItems:Object[];

public var selectedIndexes:Integer[] on replace {
selectedItems = for (i in selectedIndexes) items[i];
}

override var cellFactory = function():ListCell {
var label:Label;
def cell:ListCell = MultiSelectListCell {
onUpdate: function() {
def item = cell.item;
if (item == null) {
cell.node = null;
} else if (item instanceof Node) {
cell.node = item as Node;
} else {
if (label == null) {
label = Label { }
}
label.text = if (item instanceof String) then item as String else "{item}";
if (cell.node != label) cell.node = label;
}
}
}
}
}
[/jfx]

Moving on, we come to MultiSelectListCell, which is where the real warning comes – this class has unpublished API being used, and we offer no guarantee that it’ll stay this way. Use it at your own risk, and seriously, don’t build your business around this API being available. You’ve been warned.

This class creates a custom MultiSelectListCellSkin, and binds to the selectedIndexes sequence to determine if it is selected or not. When this property changes, it calls some impl_ code to re-evaluate its state, to allow for the background selection colour to be turned on or off as necessary. The rest of the code is pretty straightforward (i.e. don’t question it) 😉

[jfx]
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import com.sun.javafx.scene.control.skin.ListCellSkin;
import com.sun.javafx.scene.control.skin.SkinAdapter;
import javafx.util.Sequences;

public class MultiSelectListCell extends ListCell {

def multiSelected = bind Sequences.indexOf((listView as MultiSelectListView).selectedIndexes, index) != -1 on invalidate {
impl_pseudoClassStateChanged("selected");
}

package override function createDefaultSkin():Skin {
SkinAdapter {
rootRegion: MultiSelectListCellSkin { }
}
}

override function impl_getPseudoClassState():String[] {
[
if (multiSelected) "selected" else null,
super.impl_getPseudoClassState()
]
}
}
[/jfx]

MultiSelectListCellSkin is hidden inside MultiSelectListCell (for no particular reason), and its job is simply to just extend ListCellSkin, apply a small bug workaround, and apply a custom behavior, which is where the actual multiple selection magic happens.

[jfx]
class MultiSelectListCellSkin extends ListCellSkin {
override var behavior = MultiSelectListCellBehavior { }

postinit {
// This fixes an issue where the mouseReleased function is called twice.
overlay.onMouseReleased = null;
}
}
[/jfx]

The final class is a slight extension of the ListCellBehavior class, not surprisingly called MultiSelectListCellBehavior. This class handles the mouse click event, including determining if ctrl or shift is held down, and acting appropriately.

[jfx]
import com.sun.javafx.scene.control.behavior.ListCellBehavior;
import javafx.util.Sequences;

public class MultiSelectListCellBehavior extends ListCellBehavior {

override function mouseReleased(e) {
// Note that list.select will reset selection
// for out of bounds indexes. So, need to check
def listCell = skin.control as MultiSelectListCell;
def listView = listCell.listView as MultiSelectListView;

// If the mouse event is not contained within this ListCell, then
// we don’t want to react to it.
if (listCell.contains(e.x, e.y)) {
if (listCell.index >= sizeof listView.items) return;

var row = listCell.index;

if (e.controlDown) {
if (Sequences.indexOf(listView.selectedIndexes, row) == -1) {
insert row into listView.selectedIndexes;
} else {
delete row from listView.selectedIndexes;
}
} else if (e.shiftDown) {
var start = listView.focusedIndex;
var end = row;
var range = if (start < end)
then [start..end] else [end..start];

listView.selectedIndexes = range;
} else {
delete listView.selectedIndexes;
insert row into listView.selectedIndexes;
listView.focus(row);
}
}
}
}
[/jfx]

That’s all there is to it. Note that whilst I wrote this, because it is not a ‘production-quality’ control, I haven’t tested it at all, past a few user tests. I’m sure it’ll have issues. If you report them to me I’ll revise this post to ensure the best control we can have. You should also note that this in no way reflects how we’ll do multiple selection in the future, when it is supported ‘natively’.

Now, on to the warnings I warned you were coming. In general, don’t use the select() function any more – just directly manipulate the selectedIndexes sequence. Also, don’t use the selectedItem / selectedIndex properties any more – just use the selectedItems and selectedIndexes sequences instead. If you think you’re going to forget this, it should be possible to just keep overriding more functions / properties to have everything work as expected, but unless I feel sufficiently nagged, I’ll probably just leave this as a user exercise. Similarly, there is no support for keyboard navigation / multiple selection in this version.

So – that’s it really. You can download a NetBeans project containing this source code, or if you’re just wanting to use the code as-is, I’ve also put up a jar file for you to use directly in your own applications. Feel free to leave comments.