0

I have been pulling my hair out over this issue and cannot seem to find any logic fix.

I am attempting to save the contents of a table cell when the user clicks away. After following this tutorial I have table cells that save their edits when focus is lost except when the user clicks into the same column.

I know why the issue is happening, the commitEdit() method will return immediately if isEditing() is true (which it is when you click into the same column.

 public void commitEdit(T newValue) {
        if (! isEditing()) return;
    ...

I tried overriding the method to no avail. I can force update the cell contents but I don't know how to force edit the cell value without knowing what cell I am in.

If there is a way for me to get the field name of the cell I am in, I could use Reflection to force an update but I don't know how to get the field name or if it's even possible.

Chris
  • 2,435
  • 6
  • 26
  • 49
  • The tutorial you linked is way out of date. See the discussion [here](http://stackoverflow.com/questions/24694616/how-to-enable-commit-on-focuslost-for-tableview-treetableview) (Not sure what you mean by "without knowing what cell I am in" though...) – James_D Jun 06 '16 at 15:23
  • 1
    I figured it out using my own hack on a wrapper for commit with reflections. Posting answer right now – Chris Jun 06 '16 at 15:30
  • @James_D I just posted my answer, any comments, concerns, suggestions to make it better? (it works right now) – Chris Jun 06 '16 at 15:39
  • there are a bunch of bugs related to commit-on-focus-lost (on vacation, so not having my knowledge base handy, sorry - but you might search SO for some QAs), and they will NOT be fixed in jdk9. So any context-specific hack is as better than core ... be happy and keep in mind that it will probably fail anywhere else – kleopatra Jun 06 '16 at 15:54
  • @kleopatra Yea that's what I have heard. Its a shame, this is a major bug. I have been working through this issue for the better part of 2 days now so I've read most of the SO threads and seen lots of other hacks to getting around it. However i couldn't get any to fix the bug I am experiencing. – Chris Jun 06 '16 at 15:57
  • hach .. sorry, meant to comment on the answer only -forgot to delete the comment here ;-) – kleopatra Jun 06 '16 at 16:00

2 Answers2

1

It seems all you are looking for is a way for the cell to process new (or old) values and write them back to the model. Why not just provide a callback in the form of a BiConsumer<S,T>?

public class EditingCell<S,T> extends TableCell<S,T> {

    private final BiConsumer<S,T> updater ;

    public EditingCell(BiConsumer<S,T> updater) {
        this.updater = updater ;
    }

    // ....

    // not really sure what this method is for:
    public void commit(T val) {
        S rowValue = getTableView().getItems().get(getIndex());
        updater.accept(rowValue, val);
    }

    // wouldn't this be better?
    @Override
    public void commitEdit(T newValue) {
        super.commitEdit(newValue);
        S rowValue = getTableView().getItems().get(getIndex());
        updater.accept(rowValue, val);
    }

    // ...
}

Then you would do things like:

TableView<Person> table = new TableView<>();

TableColumn<Person, String> firstNameColumn = new TableColumn<>("First Name");
firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameColumn.setCellFactory(col -> new EditingCell(Person::setFirstName));
James_D
  • 201,275
  • 16
  • 291
  • 322
  • don't ... except when hacking around bugs ;-) – kleopatra Jun 06 '16 at 15:59
  • As for now, since my hack works i'm just going to keep it as is - thanks though I will definitely look back to this if I break something down the line. – Chris Jun 06 '16 at 16:00
  • Well, yeah, hacking around bugs is pretty much explicitly what we are about here... – James_D Jun 06 '16 at 16:01
  • I'm usually pretty good about hacking my way to a goal but this one was probably the most bizarre and confusing bug to hack out. Thanks though! – Chris Jun 06 '16 at 16:53
0

After digging, I found out how to get the property name of a column. With that I went ahead and wrote some generic reflections to force update. I wrapped everything in a method commit(Object val) for ease of use. These are modifications to EditCell class used here.

Disclaimer: This only works if you use a PropertyValueFactory and follow naming conventions in your row classes. This is also very fickle code, use and modify at your own discretion.

I modified the cell to be a generic cell with public static class EditingCell<S, T> extends TableCell<S, T>. Everything else from the tutorial should still be the same, if not feel free to let me know and I'll update here accordingly.

public void commit(Object val) {

        // Get the table
        TableView<S> t = this.getTableView();

        // Get the selected row/column
        S selectedRow = t.getItems().get(this.getTableRow().getIndex());
        TableColumn<S, ?> selectedColumn = t.getColumns().get(t.getColumns().indexOf(this.getTableColumn()));

        // Get current property name
        String propertyName = ((PropertyValueFactory) selectedColumn.getCellValueFactory()).getProperty();

        // Create a method name conforming to java standards ( setProperty )
        propertyName = ("" + propertyName.charAt(0)).toUpperCase() + propertyName.substring(1);

        // Try to run the update
        try {

            // Type specific checks - could be done inside each setProperty() method
            if(val instanceof Double) {
                Method method = selectedRow.getClass().getMethod("set" + propertyName, double.class);
                method.invoke(selectedRow, (double) val);
            }
            if(val instanceof String) {
                Method method = selectedRow.getClass().getMethod("set" + propertyName, String.class);
                method.invoke(selectedRow, (String) val);
            }
            if(val instanceof Integer) {
                Method method = selectedRow.getClass().getMethod("set" + propertyName, int.class);
                method.invoke(selectedRow, (int) val);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // CommitEdit for good luck
        commitEdit((T) val);
    }

and then since the text field won't update, I forced an update on it in cancelEdit(). This is slightly specific to my case ( I want a default of 0.0 and only accepted values are doubles ) - modify it as needed.

@Override
public void cancelEdit() {
    super.cancelEdit();

    // Default value
    String val = "0.0";

    // Check to see if there's a value
    if (!textField.getText().equals(""))
        val = textField.getText();

    // Set table cell text
    setText("" + val);
    setGraphic(null);
}
Chris
  • 2,435
  • 6
  • 26
  • 49
  • That will fail pretty horribly if your cell value factory is anything other than a `PropertyValueFactory`, which I frankly never use anyway. Why not just pass a `Consumer` to your custom cell implementation? – James_D Jun 06 '16 at 15:42
  • I'm not sure what a `Consumer` is as I unfortunately followed that JavaFx 2 tutorial linked in both these posts which use `PropertyValueFactory` for creating the table columns. This table I am interfacing with is created in fxml – Chris Jun 06 '16 at 15:46
  • What does FXML have to do with it? `PropertyValueFactory` is just a convenience implementation of the `Callback` required by [`setCellValueFactory`](http://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/TableColumn.html#setCellValueFactory-javafx.util.Callback-), and not a very good one at that. I was just referring to the standard Java [`Consumer`](http://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html). All you are looking for is a way to process a new value, i.e. just a function that takes a `T` as a parameter. So just provide it to the cell. – James_D Jun 06 '16 at 15:47
  • Im not entirely sure, I'm new to JavaFx in general ( < 2 weeks experience ) and I have just been following various tutorials I dig up. Both the JavaFx 8 and JavaFx 2 oracle docs for creating tables used `PropertyValueFactory` to create the tables. – Chris Jun 06 '16 at 15:49
  • Generally, It's a very bad idea to side-step the usual commit mechanics (by expicitly updating the value by whatever means in commitEdit. That said, there are a bunch of bugs related to commit-on-focus-lost (on vacation, so not having my knowledge base handy, sorry - but you might search SO for some QAs), and they will NOT be fixed in jdk9. So any context-specific hack is as better than core ... be happy and keep in mind that it will probably fail everywhere else – kleopatra Jun 06 '16 at 15:56
  • @kleopatra I agree with that. Though I think it's possible to hack a cell implementation that avoids the "cancel on focus lost" problem and still invokes the edit handlers as needed. (Register handlers on the text field to invoke `super.cancelEdit()` on escape pressed, for example; and override `cancelEdit()` to veto cancellation in cases which you don't explicitly want to actual cancel). – James_D Jun 06 '16 at 16:01
  • well, that might work (at the price of rather tricky state processing). But then, the real problem lies elsewhere (from the top of my head) - somewhere deep in CellBehavior the edit is canceled on any mousePressed on the same column as the editing column. It's a bug is a bug is a bug ... ;-) Once that's fixed (requiring api changes in TableView and Cell) everything will just work fine. It's not overly difficult to achieve cleanly, but the FX-Team is rather unwilling to tackle it (tried everything that I could think of, to no avail ;-) – kleopatra Jun 06 '16 at 16:07