TreeTableView : setting a row not editable

1.9k Views Asked by At

I want to have control over the styling of some rows of a TreeTableView based on the level in the tree. I used setRowFactory and apply a styling if this row is part of the first level children of the root of the Table. The styling works fine, but I also want to disable clicking on the checkbox for those rows. I am able to setDisable(true) but that also disables the expanding of the TreeItem and SetEditable(false) does not seem to have any effect.

EDIT: What I understand is that the Table must be set editable, then the columns are by default editable. But if I set TreeTableRow.setEditable(true); or TreeTableRow.setEditable(false); I never see any effect. The description seems of setEditable seems exactly what I want but I am unable to use it that way.

void javafx.scene.control.Cell.setEditable(boolean arg0)

setEditable public final void setEditable(boolean value)

Allows for certain cells to not be able to be edited. This is useful incases >where, say, a List has 'header rows' - it does not make sense forthe header rows >to be editable, so they should have editable set tofalse. Parameters:value - A boolean representing whether the cell is editable or not.If >true, the cell is editable, and if it is false, the cell can notbe edited.

Main:

public class TreeTableViewRowStyle extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {

        // create the treeTableView and colums
        TreeTableView<Person> ttv = new TreeTableView<Person>();
        TreeTableColumn<Person, String> colName = new TreeTableColumn<>("Name");
        TreeTableColumn<Person, Boolean> colSelected = new TreeTableColumn<>("Selected");
        colName.setPrefWidth(100);
        ttv.getColumns().add(colName);
        ttv.getColumns().add(colSelected);
        ttv.setShowRoot(false);
        ttv.setEditable(true);

        // set the columns
        colName.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
        colSelected.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(colSelected));
        colSelected.setCellValueFactory(new TreeItemPropertyValueFactory<>("selected"));

        ttv.setRowFactory(table-> {
            return new TreeTableRow<Person>(){
                @Override
                public void updateItem(Person pers, boolean empty) {
                    super.updateItem(pers, empty);
                    boolean isTopLevel = table.getRoot().getChildren().contains(treeItemProperty().get());
                    if (!isEmpty()) {
                        if(isTopLevel){
                            setStyle("-fx-background-color:lightgrey;");
                            setEditable(false); //THIS DOES NOT SEEM TO WORK AS I WANT
                            //setDisable(true); //this would disable the checkbox but also the expanding of the tree
                        }else{                              
                            setStyle("-fx-background-color:white;");
                        }
                    }
                }
            };
        });


        // creating treeItems to populate the treetableview
        TreeItem<Person> rootTreeItem = new TreeItem<Person>();
        TreeItem<Person> parent1 = new TreeItem<Person>(new Person("Parent 1"));
        TreeItem<Person> parent2 = new TreeItem<Person>(new Person("Parent 1"));
        parent1.getChildren().add(new TreeItem<Person>(new Person("Child 1")));
        parent2.getChildren().add(new TreeItem<Person>(new Person("Child 2")));
        rootTreeItem.getChildren().addAll(parent1,parent2);


        ttv.setRoot(rootTreeItem);

        // build and show the window
        Group root = new Group();
        root.getChildren().add(ttv);
        stage.setScene(new Scene(root, 300, 300));
        stage.show();
    }
}

Model Person :

public class Person {
    private StringProperty name;
    private BooleanProperty selected;

    public Person(String name) {
        this.name = new SimpleStringProperty(name);
        selected = new SimpleBooleanProperty(false);
    }

    public StringProperty nameProperty() {
        return name;
    }

    public BooleanProperty selectedProperty() {
        return selected;
    }

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

    public void setSelected(boolean selected){
        this.selected.set(selected);
    }
}
3

There are 3 best solutions below

1
On BEST ANSWER

The base problem is that none of the editable (nor the pseudo-editable like CheckBoxXX) Tree/Table cells respect the editability of the row they are contained in. Which I consider a bug.

To overcome, you have to extend the (pseudo) editable cells and make them respect the row's editable. The exact implementation is different for pseudo- vs. real editing cells. Below are in-line examples, for frequent usage you would make them top-level and re-use.

CheckBoxTreeTableCell: subclass and override updateItem to re-bind its disabled property like

colSelected.setCellFactory(c -> {
    TreeTableCell cell = new CheckBoxTreeTableCell() {

        @Override
        public void updateItem(Object item, boolean empty) {
            super.updateItem(item, empty);
            if (getGraphic() != null) {
                getGraphic().disableProperty().bind(Bindings
                        .not(
                              getTreeTableView().editableProperty()
                             .and(getTableColumn().editableProperty())
                             .and(editableProperty())
                             .and(getTreeTableRow().editableProperty())
                    ));
            }
        }

    };
    return cell;
});

For a real editing cell, f.i. TextFieldTreeTableCell: override startEdit and return without calling super if the row isn't editable

colName.setCellFactory(c -> {
    TreeTableCell cell = new TextFieldTreeTableCell() {

        @Override
        public void startEdit() {
            if (getTreeTableRow() != null && !getTreeTableRow().isEditable()) return;
            super.startEdit();
        }

    };
    return cell;
});

Now you can toggle the row's editability as you do, changed the logic a bit to guarantee full cleanup in all cases:

ttv.setRowFactory(table-> {
    return new TreeTableRow<Person>(){
        @Override
        public void updateItem(Person pers, boolean empty) {
            super.updateItem(pers, empty);
            // tbd: check for nulls!
            boolean isTopLevel = table.getRoot().getChildren().contains(treeItemProperty().get());
            if (!isEmpty() && isTopLevel) {
                //                        if(isTopLevel){
                setStyle("-fx-background-color:lightgrey;");
                setEditable(false); 
            }else{
                setEditable(true);
                setStyle("-fx-background-color:white;");

            }
        }
    };
});
5
On

If you want disable a specific Cell then handle the disable logic in the CellFactory rather than in RowFactory. The static method forTreeTableColumn(..) is a convinient method for quick use. But that is not the only way. You can still create your own factory for CheckBoxTreeTableCell.

So instead of

colSelected.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(colSelected));

set the cellfactory as below, and this should work for you.

colSelected.setCellFactory(new Callback<TreeTableColumn<Person, Boolean>, TreeTableCell<Person, Boolean>>() {

    @Override
    public TreeTableCell<Person, Boolean> call(TreeTableColumn<Person, Boolean> column) {
        return new CheckBoxTreeTableCell<Person, Boolean>() {

            @Override
            public void updateItem(Boolean item, boolean empty) {
                super.updateItem(item, empty);
                boolean isTopLevel = column.getTreeTableView().getRoot().getChildren().contains(getTreeTableRow().getTreeItem());
                setEditable(!isTopLevel);
            }
        };
    }
});
0
On

Instead of creating a custom TreeTableCell subclass you can use the following utility method that basically installs a new cell-factory on a column that delegates to the original cell-factory but adds the row-editability binding whenever a cell is created.

public <S, T> void bindCellToRowEditability(TreeTableColumn<S, T> treeTableColumn) {
    // Keep a handle on the original cell-factory.
    Callback<TreeTableColumn<S, T>, TreeTableCell<S, T>> callback = treeTableColumn.getCellFactory();
    // Install a new cell-factory that performs the delegation.
    treeTableColumn.setCellFactory(column -> {
        TreeTableCell<S, T> cell = callback.call(column);
        // Add a listener so that we pick up when a new row is set for the cell.
        cell.tableRowProperty().addListener((observable, oldRow, newRow) -> {
            // If the new row is non-null, we proceed.
            if (newRow != null) {
                // We get the cell and row editable-properties.
                BooleanProperty cellEditableProperty = cell.editableProperty();
                BooleanProperty rowEditableProperty = newRow.editableProperty();
                // Bind the cell's editable-property with its row's property.
                cellEditableProperty.bind(rowEditableProperty);
            }
        });
        return cell;
    });
}

You can then set this for all columns of your TreeTableView as:

List<TreeTableColumn<S, ?>> columns = treeTableView.getColumns();
columns.forEach(this::bindCellToRowEditability);

You still need the custom TreeTableRow that checks whether it is top-level or not so that the editable value is correctly set for the row itself. However, setting the editable value on the row will now ensure that all cells in that row correctly reflects the row's editable-property.