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 question I see occasionally is people asking how to go about using prebuilt cell factories (such as those provided in the DataFX project run by Johan Vos and I, those sitting in the OpenJFX 2.2 repo in the javafx.scene.control.cell package, or just those that they have created internally), and also show a context menu when the user right clicks. More generally, the problem is that cell factories are blackboxes, and there is no support for chaining cell factories together (or even getting hold of the cells as they are being used).

The answer is quite simple: wrap the cell factory inside another cell factory, and set the ContextMenu on the wrapping cell. In other words, you would write code such as this (for ListView):

// The cell factory you actually want to use to render the cell
Callback<ListView<T>, ListCell<T> wrappedCellFactory = ...; 

// The wrapping cell factory that will set the context menu onto the wrapped cell
Callback<ListView<T>, ListCell<T> cellFactory = new Callback<ListView<T>, ListCell<T>>() {
    @Override public ListCell<T> call(ListView<T> listView) {
        ListCell<T> cell = wrappedCellFactory  == null ? new DefaultListCell<T>() : wrappedCellFactory.call(listView);
        cell.setContextMenu(contextMenu);
        return cell;
    }
};

// Creating a ListView and setting the cell factory on it
ListView<T> listView = new ListView<T>();
listView.setCellFactory(cellFactory);

Before I get any further, I should note quickly that this blog post is about ListView / ListCell, but the exact same approach (and even code – barring a little bit of renaming) is still totally applicable to TreeView and TableView.

One thing to note in the code above is the use of DefaultListCell, which is defined below. I made a new class as I was working on this problem, but I just copy/pasted the code out of ListViewSkin for creating default ListCell instances when the end-developer has not installed a custom cell factory. DefaultListCell therefore just handles the most common case of showing text and/or a ‘graphic’ (which can be an arbitrarily complex scenegraph node):

import javafx.scene.Node;
import javafx.scene.control.ListCell;

public class DefaultListCell<T> extends ListCell<T> {
    @Override public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else if (item instanceof Node) {
            setText(null);
            Node currentNode = getGraphic();
            Node newNode = (Node) item;
            if (currentNode == null || ! currentNode.equals(newNode)) {
                setGraphic(newNode);
            }
        } else {
            setText(item == null ? "null" : item.toString());
            setGraphic(null);
        }
    }
}

Of course, I’m not going to leave you with just the code above! We have to make a more useful API out of this (this is my day job after all)! Therefore, borrowing from the style of the cell factories that are shipping with JavaFX 2.2, I present to you the following sample:

import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;

/**
 * A fully fleshed out class that allows for context menus to be shown on right click.
 */
public class ContextMenuListCell<T> extends ListCell<T> {
    
    public static <T> Callback<ListView<T>,ListCell<T>> forListView(ContextMenu contextMenu) {
        return forListView(contextMenu, null);
    }
    
    public static <T> Callback<ListView<T>,ListCell<T>> forListView(final ContextMenu contextMenu, final Callback<ListView<T>,ListCell<T>> cellFactory) {
        return new Callback<ListView<T>,ListCell<T>>() {
            @Override public ListCell<T> call(ListView<T> listView) {
                ListCell<T> cell = cellFactory == null ? new DefaultListCell<T>() : cellFactory.call(listView);
                cell.setContextMenu(contextMenu);
                return cell;
            }
        };
    }
    
    public ContextMenuListCell(ContextMenu contextMenu) {
        setContextMenu(contextMenu);
    }
}

Now that we have a simple API, we can proceed to use this API along the following lines:

// Create a MenuItem and place it in a ContextMenu
MenuItem helloWorld = new MenuItem("Hello World!");
ContextMenu contextMenu = new ContextMenu(helloWorld);

// sets a cell factory on the ListView telling it to use the previously-created ContextMenu (uses default cell factory)
listView.setCellFactory(ContextMenuListCell.<Person>forListView(contextMenu));

// Same as above, but uses a custom cell factory that is defined elsewhere 
listView.setCellFactory(ContextMenuListCell.<Person>forListView(contextMenu, customCellFactory));

This results in the following when a user right-clicks on a cell in the ListView:

However, we’re left with one final issue: we need a way to determine which cell was selected when a MenuItem action is fired. Fortunately, this proves to be trivial: simply refer to the ListView selection model, as selection also changes on right-click (which in some ways is a bug, but for now it suits our needs). Here’s some sample code:

helloWorld.setOnAction(new EventHandler<ActionEvent>() {
    @Override public void handle(ActionEvent e) {
        System.out.println("Selected item: " + listView.getSelectionModel().getSelectedItem());
    }
});

And that’s that! :-). Hopefully this is helpful to people out there. I’m sure you’ll have questions and comments – feel free to leave them on this post so that others may also learn.