0

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?

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • The NumberFormatException is actually the validation. I don't see why your solution is "clunky", looks good to me. A GUI framework doesn't have to store your previous values (how far back it has to remember, anyway?), I don't remember Swing doing that either. – m0skit0 Mar 24 '20 at 23:00
  • This is the little fellah: https://docs.oracle.com/javase/9/docs/api/javax/swing/InputVerifier.html ... `shouldYieldFocus`, all that mullarkey... ring a bell? – mike rodent Mar 24 '20 at 23:16
  • NumberFormatException is your validation here. – m0skit0 Mar 25 '20 at 08:57
  • Are you the downvoter? InputVerifier, for example, as `shouldYieldFocus` suggests, did things like prevent focus from leaving an invalid value, if one configured things that way. It may be that there is no equivalent level of sophisticated handling in JavaFX, but the question is perfectly valid and the mechanism I have found is rubbish, clunky. There's no doubt about that. – mike rodent Mar 25 '20 at 09:27
  • I'm not the downvoter. You have already 9 years in StackOverflow and still concerned about downvotes? JavaFX concerns itself with GUI only. If you want a validation library, you can look for it or write it. I prefer concise frameworks over bloated ones. Preventing the user from leaving an input is bad UX. Your opinion might differ of course. – m0skit0 Mar 25 '20 at 09:58
  • 1
    Ah, yes, the real answer it that this is a "possible dupe", not that it should be downvoted, which, yes, annoy me when the question is good and no explanation is given. Correct solution, as provided by (who else) James_D: https://stackoverflow.com/a/45079741/595305. I presume we simply do not intervene with the `StringConverter` class. PS what to do with focus should be *configurable*. – mike rodent Mar 25 '20 at 09:59
  • In fact that solution (James_D answer) doesn't work in a table context: even if you jump through hoops to obtain the "TextField" created by the `TextFieldTableCell` when editing starts, this turns out, disappointingly in view of the class name, to be a `LabeledText`, not a `TextField`, so it is not possible to set a `TextFormatter` for it. It would be necessary to re-engineer the factory, I think. – mike rodent Mar 25 '20 at 14:06

1 Answers1

1

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 than Integer. Given the following class:

class Person(){ 
...
private ObjectProperty<LocalDate> dueDate;
public void setDueDate(LocalDate value) {
    dueDateProperty().set(value);
}
public LocalDate getDueDate() {
    return (LocalDate) dueDateProperty().get();
}
public ObjectProperty dueDateProperty() {
    if (dueDate == null) dueDate = new SimpleObjectProperty(this, "dueDate");
    return dueDate;
}

Then you create a new editor cell class, which is exactly the same as TextFieldTreeTableCell (subclass of TreeTableCell), which is used by default to create an editor for a TreeTableView's table cell. However, you can't really subclass TextFieldTreeTableCell as, for example, its essential field textField is private.

So you copy the code in full from the source* (only about 30 lines), and you call it

class DueDateEditor extends TreeTableCell<Person, LocalDate> { 
    ...

You then have to create a new StringConverter class, subclassing LocalDateStringConverter. The reason for subclassing is that if you don't do that it is impossible to catch the DateTimeParseException thrown by fromString() when an invalid date is received: if you use LocalDateStringConverter the JavaFX framework unfortunately catches it, without any frames in the stack trace involving your own code. So you do this:

class ValidatingLocalDateStringConverter extends LocalDateStringConverter {
    boolean valid;
    LocalDate fromString(String value) {
        valid = true;
        if (value.isBlank()) return null;
        try {
            return LocalDate.parse(value);
        } catch (Exception e) {
            valid = false;
        }
        return null;
    }
}

Back in your DueDateEditor class you then rewrite the startEdit method as follows. NB, as with the TextFieldTreeTableCell class, textField is actually created lazily, when you first edit.

@Override
void startEdit() {
    if (! isEditable()
            || ! getTreeTableView().isEditable()
            || ! getTableColumn().isEditable()) {
        return;
    }
    super.startEdit();

    if (isEditing()) {
        if (textField == null) {
            textField = CellUtils.createTextField(this, getConverter());

            // this code added by me
            ValidatingLocalDateStringConverter converter = getConverter();
            Callable bindingFunc = new Callable(){
                @Override
                Object call() throws Exception {
                    // NB the return value from this is "captured" by the editor
                    converter.fromString( textField.getText() );
                    return converter.valid? '' : "-fx-background-color: red;";
                }
            }
            def stringBinding = Bindings.createStringBinding( bindingFunc, textField.textProperty() );
            textField.styleProperty().bind( stringBinding );


        }
        CellUtils.startEdit(this, getConverter(), null, null, textField);
    }
}

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:

Callback<TreeTableColumn, TreeTableCell> dueDateCellFactory =
        new Callback<TreeTableColumn, TreeTableCell>() {
            public TreeTableCell call(TreeTableColumn p) {
                return new DueDateEditor( new ValidatingLocalDateStringConverter() );
            }
        }
dueDateColumn.setCellFactory(dueDateCellFactory);

... the result is a nice, reactive editor cell: when containing an invalid date (acceptable pattern yyyy-mm-dd; see other LocalDate.parse() variant for other formats) the background is red, otherwise normal. Entering with a valid date works seamlessly. You can also enter an empty String, which is returned as a null LocalDate.

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 the ValidatingLocalDateStringConverter's valid field:

@Override
void commitEdit( LocalDate newDueDate ){
    if( getConverter().valid )
        super.commitEdit( newDueDate );
}

* I couldn't find this online. I extracted from the javafx source .jar file javafx-controls-11.0.2-sources.jar

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • a couple of comments (btw, this mixture of whatever, groovy?, and plain java is hard to read - not a good idea to do in an answer ;), in no particular order: a) you don't want to re-create the textField in every startEdit b) only ever really start edits if super switched the cell into editing state c) if you have a getString(item) use it always d) don't override methods that don't add anything e) _the lambda (Java) fires before the change is made to the displayed String_ which lambda? and sounds more like you using the formatter suboptimally - it can be configured to do whatever you need ;) – kleopatra Mar 26 '20 at 12:19
  • 1
    as a personal side-note - in learning a new framework/toolkit it doesn't help at all to compare bits and pieces between the old and new, they are not comparable. Been there :) Either stick to the old - and try to improve as required - or switch over whole-heartedly to the new which involves "forgetting" the ol' ways as much as possible. – kleopatra Mar 26 '20 at 12:24
  • Thanks for your constructive words. I'll put it in Java when I get a moment and apply your suggestions. Re switching over: wise approach ... if quite difficult. Bear in mind that 3 days ago I knew next-to-nothing about JavaFX Properties or factories and so am at the floundering stage. Also that no-one else has suggested an answer (nor have I found one) to what seems something that seems rather common, and maybe not Swing-specific (only experience will teach me!). – mike rodent Mar 26 '20 at 14:49