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.
2 questions:
in the first example, why do you check if cellFactory is null?
In the ContextMenuListCell class, is the constructor used for anything?
Thanks!
1) I check if the cellFactory is null just because I want to return a non-null cell, and if the cellFactory is null I need to create a DefaultListCell instead.
2) It is not used in any of the code above, but if someone wants to create their own cell factory, they can then return an instance of the ContextMenuListCell if they want (but there is very little value in doing so as any cell can have setContextMenu(..) called on it).
— Jonathan
Oh, OK, did you mean
ListCell cell = wrappedCellFactory == null ? new DefaultListCell() : wrappedCellFactory.call(listView);
?
Ah, yes, you’re quite correct. That’ll teach me to use firefox as an IDE 🙂
In my TreeView, I set a ContextMenu in my updateItem method, because I have two different types in my TreeView and I need to set a different context menu, depending on the type.
I assume this is correct application.
I have something like this:
setText(null);
setGraphic(null);
setContextMenu(null);
…
if (item instanceof ClassA)
{
setContextMenu(new ClassAContextMenu());
}
if (item instanceof ClassB)
{
setContextMenu(new ClassBContextMenu());
}
…
Each context menu uses binding to bind its MenuItems textProperty to some “global” variable, lets say one in my CellFactory.
Now the question:
Do I need to unbind those properties, before setting the context menu to null?
Otherwise I suspect that the old context menu is still bound, but there is no reference anymore, thus causing a memory leak!?
The same problem/question also can be applied to the graphic. I think before setting the graphic to null, it should be unbound.
Many thanks for this detailed post, Jonathan. I’ve been out of circulation for a time and I did not see it until today. I’ll digest it and perhaps have further comments.
I’m pretty new to both Java and Javafx, but I just wonder if it is efficient (performance-wise) to have a context menu object for each cell?
Ah, I’ve just seen that you do provide the same context menu to callbacks inside cells.. Forget this question 🙂
Hi , first of all many thanks for this post. I used your code for TreeView and it worked perfectly. Only one doubt is there in the below line:-
listView.setCellFactory(ContextMenuListCell.forListView(contextMenu, customCellFactory));
In this line i get an error at customCellFactory, to resolve error i create a local variable like this:-
Callback<TreeView, TreeCell> customCellFactory = null;
and initialize it with null.
As i am a new in java and JavaFX so can you please explain me what is customCellFactory here and for what purpose you use it. Also tell me that how to create it and initiate it.
Thanks a ton.
Hi
thanks for your great post.
i have a problem using cell factories and that is the list view is filled with images but the only thing that it shows is a memory address.
can you please help me?