0

I have a particular TreeTableView that displays a hierarchical tree of mixed types. These types do not necessarily have overlapping columns and as such the columns for some rows will be empty. As an example, consider the following classes:

public class Person {

    private final StringProperty nameProperty;
    private final StringProperty surnameProperty;

    public Person() {
        this.nameProperty = new SimpleStringProperty();
        this.surnameProperty = new SimpleStringProperty();
    }

    public StringProperty nameProperty() {
        return this.nameProperty;
    }

    public void setName(String value) {
        this.nameProperty.set(value);
    }

    public String getName() {
        return this.nameProperty.get();
    }

    public StringProperty surnameProperty() {
        return this.surnameProperty;
    }

    public void setSurname(String value) {
        this.surnameProperty.set(value);
    }

    public String getSurname() {
        return this.surnameProperty.get();
    }
}

public class Dog {

    private final StringProperty nameProperty;
    private final IntegerProperty ageProperty;
    private final StringProperty breedProperty;

    public Dog() {
        this.nameProperty = new SimpleStringProperty();
        this.ageProperty = new SimpleIntegerProperty();
        this.breedProperty = new SimpleStringProperty();
    }

    public StringProperty nameProperty() {
        return this.nameProperty;
    }

    public void setName(String value) {
        this.nameProperty.set(value);
    }

    public String getName() {
        return this.nameProperty.get();
    }

    public IntegerProperty ageProperty() {
        return this.ageProperty;
    }

    public void setAge(int value) {
        this.ageProperty.setValue(value);
    }

    public int getAge() {
        return this.ageProperty.get();
    }

    public StringProperty breedProperty() {
        return this.breedProperty;
    }

    public void setBreed(String breed) {
        this.breedProperty.set(breed);
    }

    public String getBreed() {
        return this.breedProperty.get();
    }
}

If I construct the TreeTableView as follows:

TreeTableView<Object> treeTableView = new TreeTableView<>();
treeTableView.setEditable(true);

List<TreeTableColumn<Object, ?>> columns = treeTableView.getColumns();

TreeTableColumn<Object, String> nameColumn = new TreeTableColumn<>("Name");
nameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
nameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
columns.add(nameColumn);

TreeTableColumn<Object, String> surnameColumn = new TreeTableColumn<>("Surname");
surnameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
surnameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("surname"));
columns.add(surnameColumn);

TreeTableColumn<Object, Integer> ageColumn = new TreeTableColumn<>("Age");
ageColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn(new IntegerStringConverter()));
ageColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("age"));
columns.add(ageColumn);

TreeTableColumn<Object, String> breedColumn = new TreeTableColumn<>("Breed");
breedColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
breedColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("breed"));
columns.add(breedColumn);

TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);

List<TreeItem<Object>> rootChildren = rootItem.getChildren();

Person john = new Person();
john.setName("John");
john.setSurname("Denver");
TreeItem<Object> johnTreeItem = new TreeItem<>(john);

rootChildren.add(johnTreeItem);

List<TreeItem<Object>> johnChildren = johnTreeItem.getChildren();

Dog charlie = new Dog();
charlie.setName("Charlie");
charlie.setAge(4);
charlie.setBreed("Labrador");
TreeItem<Object> charlieTreeItem = new TreeItem<>(charlie);
johnChildren.add(charlieTreeItem);

Dog daisy = new Dog();
daisy.setName("Daisy");
daisy.setAge(7);
daisy.setBreed("Bulldog");
TreeItem<Object> daisyTreeItem = new TreeItem<>(daisy);
johnChildren.add(daisyTreeItem);

I will get a TreeTableView that looks like:

enter image description here

The Age and Breed columns are empty for the TreeItems that contains Person objects. However, nothing stops me from editing Age or Breed cell for the top-most Person row. Setting a value in one of those cells doesn't change the Person object, but the value still hangs around there like it is committed.

Is there any way to prevent this from happening? I know that I could check for nulls in a custom TreeTableCell subclass and prevent the editing from kicking off in the startEdit() method. However, there are circumstances where a null-value is valid and preventing editing by checking nulls is not a feasible solution for all situations. Also, creating a custom TreeTableCell subclass for every datatype and corresponding columns is painful. It would have been nice if TreeItemPropertyValueFactory could provide for a way to abort the edit when no value is present for a particular cell.

wcmatthysen
  • 445
  • 4
  • 19
  • Does this answer your question? [TreeTableView : setting a row not editable](https://stackoverflow.com/questions/52528697/treetableview-setting-a-row-not-editable) – kleopatra Nov 18 '19 at 12:12
  • No, it doesn't. The question you are referring to has to do with the editability of an entire row and to sync it up with the editability of a cell. My question has to do with how to prevent a cell from being edited if I make use of TreeItemPropertyValueFactory and it doesn't return a value for some cells. – wcmatthysen Nov 18 '19 at 12:58
  • but it's basically the same ... just bind the cell's editability to the type of the item or to whatever you want. That's not more than applying your answer there to your problem here. – kleopatra Nov 18 '19 at 14:55
  • I guess you're right. But, going that route is going to be messy. I was hoping there was a cleaner way where using TreeItemPropertyValueFactory would allow for prematurely exiting cell-editing when no value is present for a cell. – wcmatthysen Nov 18 '19 at 15:04
  • the valueFactory != cellFactory :) What you need is a custom cell that binds its own editability to whatever criterion you deem appropriate. – kleopatra Nov 18 '19 at 15:06
  • Jip, I know that valueFactory is not the same as cellFactory. However, in this case it might be worth it to create a custom cellFactory that knows about the valueFactory (in particular TreeItemPropertyValueFactory) to determine whether the cell should be editable or not, and then to add a binding to the cell's editability (almost like the cell->row editability). The problem with creating a custom cell is that this is not feasible in all situations as you might deal with 3rd-party code where you are using a custom cell whose class is marked as final. – wcmatthysen Nov 18 '19 at 16:19
  • hmm ... no, if you are mixing view state into a data object (though they aren't so well separated for tree/Table) you are looking into the wrong direction. Third-party for cells, really? There isn't that much a cell can do (at least it shouldn't, IMO ;) Anyway, if you want to clarify your requirements, best do so in the question (vs. in comments which might be deleted sooner or later) – kleopatra Nov 18 '19 at 16:39
  • I'm using 3rd-party as an example. You might come across a cell implementation class that wasn't designed to be inherited from. Having to extend from each and every cell implementation class to add this bit of logic is painful. It would have been nice if there was a way in which the cell-editing could abort in this particular case. – wcmatthysen Nov 18 '19 at 16:57
  • If you look at the TreeItemPropertyValueFactory class' implementation details, the part there at the end where the exception is logged when no value is present. It would have been nice if we could have control to not fall-through and just return null, but to have an alternative option of aborting the edit or something. But, I know that the editing is related to the cell itself and not its value, so this will be mixing view-state and data-object as you mentioned, but the end result would have been really convenient to use. – wcmatthysen Nov 18 '19 at 17:00

1 Answers1

0

Ok, I scraped together something by looking at the TreeItemPropertyValueFactory class itself for inspiration. This gives me the desired functionality, although I'm not sure if it is 100% correct or what the implications are of using it.

It basically comes down to installing a new cell-factory that checks if the cell-value-factory is of type TreeItemPropertyValueFactory. If it is the case, a new cell-factory is installed that delegates to the original but adds listeners for the table-row and tree-item properties. When the TreeItem changes, we get the row-data and see if we can access the desired property (via a PropertyReference that is cached for performance). If we can't (and we get the two exceptions) we assume that the property cannot be accessed and we set the cell's editable-property to false.

public <S, T> void disableUnavailableCells(TreeTableColumn<S, T> treeTableColumn) {
    Callback<TreeTableColumn<S, T>, TreeTableCell<S, T>> cellFactory = treeTableColumn.getCellFactory();
    Callback<CellDataFeatures<S, T>, ObservableValue<T>> cellValueFactory = treeTableColumn.getCellValueFactory();
    if (cellValueFactory instanceof TreeItemPropertyValueFactory) {
        TreeItemPropertyValueFactory<S, T> valueFactory = (TreeItemPropertyValueFactory<S, T>)cellValueFactory;
        String property = valueFactory.getProperty();
        Map<Class<?>, PropertyReference<T>> propertyRefCache = new HashMap<>();
        treeTableColumn.setCellFactory(column -> {
            TreeTableCell<S, T> cell = cellFactory.call(column);
            cell.tableRowProperty().addListener((o1, oldRow, newRow) -> {
                if (newRow != null) {
                    newRow.treeItemProperty().addListener((o2, oldTreeItem, newTreeItem) -> {
                        if (newTreeItem != null) {
                            S rowData = newTreeItem.getValue();
                            if (rowData != null) {
                                Class<?> rowType = rowData.getClass();
                                PropertyReference<T> reference = propertyRefCache.get(rowType);
                                if (reference == null) {
                                    reference = new PropertyReference<>(rowType, property);
                                    propertyRefCache.put(rowType, reference);
                                }
                                try {
                                    reference.getProperty(rowData);
                                } catch (IllegalStateException e1) {
                                    try {
                                        reference.get(rowData);
                                    } catch (IllegalStateException e2) {
                                        cell.setEditable(false);
                                    }
                                }
                            }
                        }
                    });
                }
            });
            return cell;
        });
    }
}

For the example listed in the question, you can call it after you created all your columns as:

...
columns.forEach(this::disableUnavailableCells);

TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);
...

You'll see that cells for the Age and Breed columns are now uneditable for Person entries whereas cells for the Surname column is now uneditable for Dog entries, which is what we want. Cells for the common Name column is editable for all entries as this is a common property among Person and Dog objects.

wcmatthysen
  • 445
  • 4
  • 19