Custom editor components in JavaFX TableCells

October 27, 2014 by Michael

There’s a lot of confusion about custom editor components in TableCells for JavaFX applications. The problem: Someone (like me) implements a custom TableCell with a DatePicker or a ColorPicker for example. A naive solution maybe looks like this.

That works in so far that the value is correctly displayed in the ColorPicker and the ColorPicker is active and can be changed. But, given that the cell is editable, the property of the cells item is editable, the value of it never changes, regardless what is selected in the ColorPicker.

You’ll find a lot of tips like this or this. They have in common that they are either rather complex and most of the time uses a stanza like this:

TableCell cell;
cell.contentDisplayProperty().bind(Bindings.when(editingProperty())
                    .then(ContentDisplay.GRAPHIC_ONLY)
                    .otherwise(ContentDisplay.TEXT_ONLY));

Although pretty cool feature that bindings can be conditionally expressed, but not so useful. What is stated there is: “If the table is not in editing mode, use the text set with setText (in that case a formatted date), otherwise, if editing mode, use the node provided through setGraphic”. So the date picker in that case or my color picker is only available when editing. What might work with a date picker isn’t an option with the color picker. Also, the user need to do 2 clicks.

So i came up with that solution:

public class ColorTableCell<T> extends TableCell<T, Color> {    
    private final ColorPicker colorPicker;
 
    public ColorTableCell(TableColumn<T, Color> column) {
	this.colorPicker = new ColorPicker();
	this.colorPicker.editableProperty().bind(column.editableProperty());
	this.colorPicker.disableProperty().bind(column.editableProperty().not());
	this.colorPicker.setOnShowing(event -> {
	    final TableView<T> tableView = getTableView();
	    tableView.getSelectionModel().select(getTableRow().getIndex());
	    tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);	    
	});
	this.colorPicker.valueProperty().addListener((observable, oldValue, newValue) -> {
	    if(isEditing()) {
		commitEdit(newValue);
	    }
	});		
	setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }
 
    @Override
    protected void updateItem(Color item, boolean empty) {
	super.updateItem(item, empty);	
 
	setText(null);	
	if(empty) {	    
	    setGraphic(null);
	} else {	    
	    this.colorPicker.setValue(item);
	    this.setGraphic(this.colorPicker);
	} 
    }
}

Let me explain:

  1. this.colorPicker.setOnShowing() adds a handler that is executed just before the color picker popups up. It retrieves the TableView from the cell and also the currently selected row (which only works in single selection mode). It than starts the edit by calling tableView.edit. There’s no need to call startEdit() from TableCell, which actually will lead to a NPE somewhere…
  2. Than i watch the valueProperty() of the color picker and not the onAction or keypressed or whatsoever event, i want the value. That value is then committed. All further actions on the table are automatically, no need to manually call updateItem and the like.

That solution can be easily transferred to LocalDate cells etc. It’s benefits: It’s trivial and easy to implement, better user experience (no vanishing controls, no 2 clicks). Everything that fiddles with setContentDisplay(text or graphic) is just working around to get the table into edit mode when the user clicks often enough.

The classes shown here are part of my JavaFX demo project BikingFX, which is available on GitHub.

What do you think?

19 comments

  1. Jan Petersen wrote:

    Excellent approach. It helped me creating a ChoiceBoxTableCell around the same concept.

    —-

    import javafx.collections.ObservableList;
    import javafx.scene.control.ChoiceBox;
    import javafx.scene.control.ContentDisplay;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
     
    public class ChoiceBoxTableCell extends TableCell {
     
    	private final ChoiceBox box;
    	private boolean selectRowOnClick = true;
     
    	public ChoiceBoxTableCell(TableColumn column, ObservableList choiceList) {
    		this.box = new ChoiceBox(choiceList);
    		this.box.disableProperty().bind(column.editableProperty().not());
    		this.box.showingProperty().addListener(event -&gt; {
    			final TableView tableView = getTableView();
    			if (selectRowOnClick) {
    				tableView.getSelectionModel().select(getTableRow().getIndex());
    				tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
    			} else {
    				tableView.edit(getTableRow().getIndex(), column);
    			}
    		});
    		this.box.valueProperty().addListener(
    			(observable, oldValue, newValue) -&gt; {
    				if (isEditing()) {
    					commitEdit(newValue);
    				}
    			});
    		setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    	}
     
    	public ChoiceBoxTableCell selectRowOnClick(boolean selectRowOnClick) {
    		this.selectRowOnClick = selectRowOnClick;
    		return this;
    	}
     
    	@Override
    	protected void updateItem(E item, boolean empty) {
    		super.updateItem(item, empty);
     
    		setText(null);
    	     if (empty || item == null) {
    	         setGraphic(null);
    		} else {
    			this.box.setValue(item);
    			this.setGraphic(this.box);
    		}
    	}
     
    }
    Posted on June 27, 2015 at 11:53 AM | Permalink
  2. Michael wrote:

    Hello Jan,

    great feedback, thanks.

    I took the freedom to enable syntax highlighting on your code.

    Have a great day,
    Michael.

    Posted on June 27, 2015 at 12:37 PM | Permalink
  3. Daniele wrote:

    Hi,
    great post! I’m using the LocalDateTableCell; it works fine if you select the value from the picker, but if you try to modify manually the text and then confirm it pressing enter key, your change is lost and is preserved last value of date.

    How you suggest to solve the problem?

    Thanks

    Posted on November 11, 2015 at 7:40 PM | Permalink
  4. Michael wrote:

    Hi Daniele,

    which LocalDateTableCell do you use? I have thought that it *would* work, didn’t implement it myself…

    Posted on November 11, 2015 at 8:30 PM | Permalink
  5. Daniele wrote:

    Hi Michael,
    I used this class https://github.com/michael-simons/bikingFX/blob/master/src/main/java/ac/simons/bikingFX/common/LocalDateTableCell.java

    It works quite well but if you change the date by hand in the textbox the value is not committed. I think is missing a listener of keyReleased (ENTER key) and a listener on loss of focus that should commit the value of the textbox.

    THanks

    Posted on November 11, 2015 at 10:07 PM | Permalink
  6. Michael wrote:

    Thanks Daniele,
    thanks for reminding me what I have written… How embarrassing. I’ll have a look tomorrow.

    Posted on November 11, 2015 at 10:20 PM | Permalink
  7. Daniele wrote:

    🙂 Thanks very much!

    Posted on November 11, 2015 at 10:23 PM | Permalink
  8. Michael wrote:

    Hello Daniele,

    i’ve just updated the component.

    Editing starts when the editor component receives focus as well as the button is clicked. See https://github.com/michael-simons/bikingFX/commit/b9129f0ef23bef765ed1deaf68cdcb2fc8e4b2e8#diff-188032c25dbef0578d1639b72a285224.

    So if you click inside the text field and you enter a valid date and commit with ENTER, everything is fine.
    Regarding commit on focus lost see https://bugs.openjdk.java.net/browse/JDK-8136838, i don’t thing i’m gonna put effort into this.
    If you find a solution, I’d be more then happy to add it.

    have a great day,
    Michael

    Posted on November 12, 2015 at 11:08 AM | Permalink
  9. Daniele wrote:

    Hi,
    thanks for the update. I had done something slightly different to handle the loss of focus: http://pastie.org/private/i18lxfg8l5hkzy0o2sxx3g

    What do you think?

    Posted on November 12, 2015 at 11:29 AM | Permalink
  10. Michael wrote:

    Lines 29 – 34 are obsolete with my update (i have replaced that code with the focus property).

    You don’t need lines 35 – 53, this works out of the box (hitting enter ).

    Lines 64 – 73: That’s would i would do as well for the focus lost event, but haven’t done it because it would fire twice when using the button.

    Posted on November 12, 2015 at 11:32 AM | Permalink
  11. Daniele wrote:

    Thanks very much, all very clear!

    Posted on November 12, 2015 at 11:45 AM | Permalink
  12. Michael wrote:

    Happy to hear! Thanks for reading my blog.

    Posted on November 12, 2015 at 11:46 AM | Permalink
  13. Farhan Ali wrote:

    How can you implement with ComboBox, I tried this logivlc to implement ComboBox editor but I am getting CalssCastException that String can’t be cast to my custom object.
    This is happening at commitEdit.

    Regards,

    Farhan

    Posted on November 28, 2015 at 12:02 AM | Permalink
  14. Michael wrote:

    Hi Farhan,
    that sounds that you have only Strings in your ComboBox Model (the List that is the data source of your view).

    You have two options: Add your custom Objects to the ComboBox’ model. Then you could either implement a fancy toString on the model or, the “cleaner” solution, implement an additional CellFactory for customizing the rendering.

    Posted on November 29, 2015 at 10:47 AM | Permalink
  15. Mike wrote:

    I like the way you wrote your color picker table cell. Very simple and a nice display. Thanks for sharing your code.

    Posted on January 13, 2016 at 11:43 PM | Permalink
  16. Michael wrote:

    Thanks, Mike!

    Posted on January 14, 2016 at 6:32 AM | Permalink
  17. Walderi M. W. Filho wrote:

    Thanks man, you save my life.

    Posted on March 14, 2016 at 6:05 PM | Permalink
  18. Patrik wrote:

    Hi.

    This is a really useful post. But, it turns out it’s much harder to implement a custom ComboBox table cell. To be more specific, I tried it and I’m getting this problem:

    http://stackoverflow.com/quest.....e-behavior

    Can you help me please? I’m really stucked and can’t figure this out. Thank you

    Posted on November 15, 2016 at 7:30 PM | Permalink
  19. Miroslav Krýsl wrote:

    Hi, i tried this solution and it has worked.
    Instead of passing column instance through constructor just use TableCell class methods getTableView(), getTableRow() and getTableColumn():

    this.colorPicker.setOnShowing(event -> {
    getTableView().edit(getTableRow().getIndex(), getTableColumn());
    });

    Posted on May 28, 2017 at 8:07 PM | Permalink
Post a Comment

Your email is never published nor shared. Required fields are marked *