Edit:
I first voted to close as a duplicate after finding this answer by James_D, which sets a TextFormatter on a TextField. But then firstly I found that (in a TableView context) the method TextFieldTableCell.forTableColumn() does not in fact draw a TextField when it starts editing, but instead a LabeledText, which does not subclass TextInputControl, and therefore does not have setTextFormatter().
Secondly, I wanted something which acted in a familiar sort of way. I may have produced the "canonical" solution in my answer: let others judge.
This is a TableColumn in a TableView (all Groovy):
TableColumn<Person, String> ageCol = new TableColumn("Age")
ageCol.cellValueFactory = { cdf -> cdf.value.ageProperty() }
int oldAgeValue
ageCol.onEditStart = new EventHandler(){
@Override
public void handle( Event event) {
oldAgeValue = event.oldValue
}
}
ageCol.cellFactory = TextFieldTableCell.forTableColumn(new IntegerStringConverter() {
@Override
public Integer fromString(String value) {
try {
return super.fromString(value)
}
catch ( NumberFormatException e) {
// inform user by some means...
println "string could not be parsed as integer..."
// ... and cancel the edit
return oldAgeValue
}
}
})
Excerpt from class Person:
public class Person {
private IntegerProperty age;
public void setAge(Integer value) { ageProperty().set(value) }
public Integer getAge() { return ageProperty().get() }
public IntegerProperty ageProperty() {
if (age == null) age = new SimpleIntegerProperty(this, "age")
return age
}
...
Without the start-edit Handler, when I enter a String which can't be parsed as an Integer NumberFormatException not surprisingly gets thrown. But I also find that the number in the cell then gets set to 0, which is likely not to be the desired outcome.
But the above strikes me as a pretty clunky solution.
I had a look at ageCol, and ageCol.cellFactory (as these are accessible from inside the catch block) but couldn't see anything better and obvious. I can also see that one can easily obtain the Callback (ageCol.cellFactory), but calling it would require the parameter cdf, i.e. the CellDataFeatures instance, which again you'd have to store somewhere.
I'm sure a validator mechanism of some kind was involved with Swing: i.e. before a value could be transferred from the editor component (via some delegate or something), it was possible to override some validating mechanism. But this IntegerStringConverter seems to function as a validator, although doesn't seem to provide any way to revert to the existing ("old") value if validation fails.
Is there a less clunky mechanism than the one I've shown above?
Edit
NB improved after kleopatra's valuable insights.
Edit2
Overhauled completely after realising that the best thing is to use the existing default editor and tweak it.
I thought I'd give an example with a
LocalDate, slightly more fun thanInteger. Given the following class:Then you create a new editor cell class, which is exactly the same as
TextFieldTreeTableCell(subclass ofTreeTableCell), which is used by default to create an editor for aTreeTableView's table cell. However, you can't really subclassTextFieldTreeTableCellas, for example, its essential fieldtextFieldisprivate.So you copy the code in full from the source* (only about 30 lines), and you call it
You then have to create a new
StringConverterclass, subclassingLocalDateStringConverter. The reason for subclassing is that if you don't do that it is impossible to catch theDateTimeParseExceptionthrown byfromString()when an invalid date is received: if you useLocalDateStringConverterthe JavaFX framework unfortunately catches it, without any frames in the stack trace involving your own code. So you do this:Back in your
DueDateEditorclass you then rewrite thestartEditmethod as follows. NB, as with theTextFieldTreeTableCellclass,textFieldis actually created lazily, when you first edit.NB don't bother trying to look up
CellUtils: this is package-private, the package in question being javafx.scene.control.cell.To set things up you do this:
... the result is a nice, reactive editor cell: when containing an invalid date (acceptable pattern
yyyy-mm-dd; see otherLocalDate.parse()variant for other formats) the background is red, otherwise normal. Entering with a valid date works seamlessly. You can also enter an emptyString, which is returned as anullLocalDate.With the above, pressing Enter with an invalid date sets the date to
null. But overriding things to prevent this happening (i.e. forcing you to enter a valid date, or cancel the edit, e.g. by Escape) is trivial, using theValidatingLocalDateStringConverter'svalidfield:* I couldn't find this online. I extracted from the javafx source .jar file javafx-controls-11.0.2-sources.jar