Search code examples
javacheckboxjavafx-8treetableview

JavaFX: Add CheckBoxTreeItem in TreeTable?


I am experimenting with JavaFX and I am trying to add a check box Item in tree table, but it looks like it supports only simple tree item.

My Code is modified version of Oracle's TreeTableView Example:

 public class TreeTableViewSample extends Application implements Runnable {

List<Employee> employees = Arrays.<Employee>asList(
        new Employee("Ethan Williams", 30.0),
        new Employee("Emma Jones", 10.0),
        new Employee("Michael Brown", 70.0),
        new Employee("Anna Black", 50.0),
        new Employee("Rodger York", 20.0),
        new Employee("Susan Collins", 70.0));

/*  private final ImageView depIcon = new ImageView (
 new Image(getClass().getResourceAsStream("department.png"))
 );
 */
final CheckBoxTreeItem<Employee> root
        = new CheckBoxTreeItem<>(new Employee("Sales Department", 0.0));
final CheckBoxTreeItem<Employee> root2
        = new CheckBoxTreeItem<>(new Employee("Departments", 0.0));

public static void main(String[] args) {
    Application.launch(TreeTableViewSample.class, args);
}

@Override
public void start(Stage stage) {
    root.setExpanded(true);
    employees.stream().forEach((employee) -> {
        root.getChildren().add(new CheckBoxTreeItem<>(employee));
    });
    stage.setTitle("Tree Table View Sample");
    final Scene scene = new Scene(new Group(), 400, 400);
    scene.setFill(Color.LIGHTGRAY);
    Group sceneRoot = (Group) scene.getRoot();

    TreeTableColumn<Employee, String> empColumn
            = new TreeTableColumn<>("Employee");
    empColumn.setPrefWidth(150);
    empColumn.setCellValueFactory(
            (TreeTableColumn.CellDataFeatures<Employee, String> param)
            -> new ReadOnlyStringWrapper(param.getValue().getValue().getName())
    );

    TreeTableColumn<Employee, Double> salaryColumn
            = new TreeTableColumn<>("Salary");
    salaryColumn.setPrefWidth(190);
    /*   salaryColumn.setCellValueFactory(
     (TreeTableColumn.CellDataFeatures<Employee, String> param) -> 
     new ReadOnlyDoubleWrapper(param.getValue().getValue().getEmail())
     );
     */
    salaryColumn.setCellFactory(ProgressBarTreeTableCell.<Employee>forTreeTableColumn());
    root2.getChildren().add(root);

    TreeTableView<Employee> treeTableView = new TreeTableView<>(root2);
    treeTableView.getColumns().setAll(empColumn, salaryColumn);
    sceneRoot.getChildren().add(treeTableView);
    stage.setScene(scene);
    stage.show();
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    executorService.scheduleAtFixedRate(this, 3, 10, TimeUnit.SECONDS);

}

@Override
public void run() {
    root2.getValue().setSalary(calcSalary(root));
}

public double calcSalary(TreeItem<Employee> t) {
    Double salary = 0.0;
    if (!t.isLeaf()) {

        ObservableList<TreeItem<Employee>> al = t.getChildren();
        for (int i = 0; i < al.size(); i++) {
            TreeItem<Employee> get = al.get(i);
            salary += calcSalary(get);
        }
        t.getValue().setSalary(salary);
    }
    return salary += t.getValue().getSalary();
}

public class Employee {

    private SimpleStringProperty name;
    private SimpleDoubleProperty salary;

    public SimpleStringProperty nameProperty() {
        if (name == null) {
            name = new SimpleStringProperty(this, "name");
        }
        return name;
    }

    public SimpleDoubleProperty salaryProperty() {
        if (salary == null) {
            salary = new SimpleDoubleProperty(this, "salary");
        }
        return salary;
    }

    private Employee(String name, Double salary) {
        this.name = new SimpleStringProperty(name);
        this.salary = new SimpleDoubleProperty(salary);
    }

    public String getName() {
        return name.get();
    }

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

    public Double getSalary() {
        return salary.get();
    }

    public void setSalary(Double fName) {
        salary.set(fName);
    }
}
}

Is there any way i can use checkboxes for tree item in the above example? I am using JavaFx 8.

I am also try to create Salary Bars, which can also be used as progressbar for a task and its sub tasks. (Just playing with UI). But don't know how to connect them with the real values of employe, as i guess normal table view is different from tree table view. Thanks ! :)


Solution

  • There is no cell implemenation that corresponds to CheckBoxTreeCell: that is a cell with a checkBox that is bound to the selected/indeterminate property of a CheckBoxTreeItem. The apparent counterpart CheckBoxTreeTableCell is simply a cell with a checkbox, that's bound to the cell data.

    What's needed is a CheckBoxTreeTableRow: that's the cell layer that has access to the TreeItem and can manage the bindings between the checkBox and the treeItem. Below is a quick implementation, simplified and adjusted copy of CheckBoxTreeCell. The un/binding is handled in updateItem.

    Update: Clean solution (lengthy!)

    It looks like TableRowSkinBase is prepared to handle custom row graphics, it has an method graphicsProperty() which is used in all layout code inside the row skin.

    /**
     * Returns the graphic to draw on the inside of the disclosure node. Null
     * is acceptable when no graphic should be shown. Commonly this is the
     * graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather
     * than a graphic associated with a cell.
     */
    protected abstract ObjectProperty<Node> graphicProperty();
    

    TreeTableRowSkin implements it to return the graphic of the TreeItem, so overriding to return the graphic of the tableRow should be working. Except ... it isn't - layout is crooked as noted in the original hacky answer below. Digging exposed the culprit: it's TreeTableCellSkin which hard-codes it own layout code to account for any graphics in its padding to ... the treeItem's graphic.

    So a complete solution needs

    • a Tree/TableCellSkin that doesn't hard-code the treeItem graphics (the example below is still not entirely clean, it relies on super having added the graphics width and substracts it again)
    • a Tree/TableCell that installs the enhanced skin
    • a Tree/TableRowSkin that overrides graphicsProperty as needed, below returning the row graphic
    • a Tree/TableRow that updates its graphics as needed, below setting its graphic to a checkBox which in turn might contain the treeItem's graphic

    The first couple is named DefaultTreeTableCell/Skin, the second CheckBoxTreeTableRow/Skin below.

    Usage (snippets to insert into OPs example)

    // just for fun, have root items with some graphic
    final CheckBoxTreeItem<Employee> root = new CheckBoxTreeItem<>(
            new Employee("Sales Department", 0.0), new Circle(10, Color.RED));
    final CheckBoxTreeItem<Employee> root2 = new CheckBoxTreeItem<>(
            new Employee("Departments", 0.0), new Circle(10, Color.BLUE));
    
    // configure treeTableView to use the extended tableRow 
    treeTableView.setRowFactory(item -> new CheckBoxTreeTableRow<>());
    
    // configure table columns to use the extended table cell
    empColumn.setCellFactory(p -> new DefaultTreeTableCell<>());
    // all cell types must have a skin that copes with row graphics
    salaryColumn.setCellFactory(e -> {
        TreeTableCell cell = new ProgressBarTreeTableCell() {
    
            @Override
            protected Skin<?> createDefaultSkin() {
                return new DefaultTreeTableCell.DefaultTreeTableCellSkin<>(this);
            }
    
        };
        return cell;
    });
    

    Cell/Row implementaions:

    /**
     * TreeTableCell actually showing something. This is copied from TreeTableColumn plus
     * installs DefaultTreeTableCellSkin which handles row graphic width.
     */
    public class DefaultTreeTableCell<S, T> extends TreeTableCell<S, T> {
    
        @Override
        protected void updateItem(T item, boolean empty) {
            if (item == getItem()) return;
    
            super.updateItem(item, empty);
    
            if (item == null) {
                super.setText(null);
                super.setGraphic(null);
            } else if (item instanceof Node) {
                super.setText(null);
                super.setGraphic((Node)item);
            } else {
                super.setText(item.toString());
                super.setGraphic(null);
            }
        }
    
        @Override
        protected Skin<?> createDefaultSkin() {
            return new DefaultTreeTableCellSkin<>(this);
        }
    
        /**
         * TreeTableCellSkin that handles row graphic in its leftPadding, if
         * it is in the treeColumn of the associated TreeTableView.
         * <p>
         * It assumes that per-row graphics - including the graphic of the TreeItem, if any -
         * is folded into the TreeTableRow graphic and patches its leftLabelPadding
         * to account for the graphic width.
         * <p>
         * 
         * Note: TableRowSkinBase seems to be designed to cope with variations of row 
         * graphic - it has a method <code>graphicProperty()</code> that's always used
         * internally when calculating offsets in the treeColumn.
         * Subclasses override as needed, the layout code remains constant. The real 
         * problem is the TreeTableCell hard-codes the TreeItem as the only graphic
         * owner. 
         *  
         */
        public static class DefaultTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
    
            /**
             * @param treeTableCell
             */
            public DefaultTreeTableCellSkin(TreeTableCell<S, T> treeTableCell) {
                super(treeTableCell);
            }
    
            /**
             * Overridden to adjust the padding returned by super for row graphic.
             */
            @Override
            protected double leftLabelPadding() {
                double padding = super.leftLabelPadding();
                padding += getRowGraphicPatch();
                return padding;
            }
    
            /**
             * Returns the patch for leftPadding if the tableRow has a graphic of
             * its own.<p>
             * 
             * Note: this implemenation is a bit whacky as it relies on super's 
             * handling of treeItems graphics offset. A cleaner 
             * implementation would override leftLabelPadding from scratch.
             * <p>
             * PENDING JW: doooooo it!
             * 
             * @return
             */
            protected double getRowGraphicPatch() {
                if (!isTreeColumn()) return 0;
                Node graphic = getSkinnable().getTreeTableRow().getGraphic();
                if (graphic != null) {
                    double height = getCellSize();
                    // start with row's graphic
                    double patch = graphic.prefWidth(height);
                    // correct for super's having added treeItem's graphic
                    TreeItem<S> item = getSkinnable().getTreeTableRow().getTreeItem();
                    if (item.getGraphic() != null) {
                        double correct = item.getGraphic().prefWidth(height);
                        patch -= correct;
                    }
                    return patch;
                }
                return 0;
            }
    
            /**
             * Checks and returns whether our cell is attached to a treeTableView/column
             * and actually has a TreeItem.
             * @return
             */
            protected boolean isTreeColumn() {
                if (getSkinnable().isEmpty()) return false;
                TreeTableColumn<S, T> column = getSkinnable().getTableColumn();
                TreeTableView<S> view = getSkinnable().getTreeTableView();
                if (column.equals(view.getTreeColumn())) return true;
                return view.getVisibleLeafColumns().indexOf(column) == 0;
            }
    
        }
    
    }
    
    /**
     * Support custom graphic for Tree/TableRow. Here in particular a checkBox.
     * http://stackoverflow.com/q/29300551/203657
     * <p>
     * Basic idea: implement custom TreeTableRow that set's its graphic to the 
     * graphic/checkBox. Doesn't work: layout is broken, graphic appears
     * over the text. All fine if we set the graphic to the TreeItem that's
     * shown. Possible as long as the treeItem doesn't have a graphic of
     * its own.
     * <p>
     * Basic problem:
     * <li> TableRowSkinBase seems to be able to cope: has protected method
     *   graphicsProperty that should be implemented to return the graphic 
     *   if any. That graphic is added to the children list and sized/located
     *   in layoutChildren. 
     * <li> are added the graphic/disclosureNode as needed before
     *   calling super.layoutChildren,  
     * <li> graphic/disclosure are placed inside the leftPadding of the tableCell
     *   that is the treeColumn
     * <li> TreeTableCellSkin must cooperate in taking into account the graphic/disclosure 
     *   when calculating its leftPadding
     * <li> cellSkin is hard-coded to use the TreeItem's graphic (vs. the rowCell's)   
     *
     * PENDING JW: 
     * <li>- would expect to not alter the scenegraph during layout (might lead to
     *   endless loops or not) but done frequently in core code   
     * <p> 
     *  
     * Outline of the solution as implemented:
     * <li> need a TreeTableCell with a custom skin
     * <li> override leftPadding in skin to add row graphic if available
     * <li> need CheckBoxTreeTableRow that sets its graphic to checkBox (or a combination
     *   of checkBox and treeItem's)
     * <li> need custom rowSkin that implements graphicProperty to return the row graphic  
     *    
     * @author Jeanette Winzenburg, Berlin
     * 
     * @see DefaultTreeTableCell
     * @see DefaultTreeTableCellSkin
     * 
     */
    public class CheckBoxTreeTableRow<T> extends TreeTableRow<T> {
    
        private CheckBox checkBox;
    
        private ObservableValue<Boolean> booleanProperty;
    
        private BooleanProperty indeterminateProperty;
    
        public CheckBoxTreeTableRow() {
            this(item -> {
                if (item instanceof CheckBoxTreeItem<?>) {
                    return ((CheckBoxTreeItem<?>)item).selectedProperty();
                }
                return null;
            });
        }
    
        public CheckBoxTreeTableRow(
                final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedProperty) {
            this.getStyleClass().add("check-box-tree-cell");
            setSelectedStateCallback(getSelectedProperty);
            checkBox = new CheckBox();
            checkBox.setAlignment(Pos.TOP_LEFT);
        }
    
        // --- selected state callback property
        private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> 
                selectedStateCallback = 
                new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
                this, "selectedStateCallback");
    
        /**
         * Property representing the {@link Callback} that is bound to by the 
         * CheckBox shown on screen.
         */
        public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() { 
            return selectedStateCallback; 
        }
    
        /** 
         * Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
         */
        public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) { 
            selectedStateCallbackProperty().set(value); 
        }
    
        /**
         * Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
         */
        public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() { 
            return selectedStateCallbackProperty().get(); 
        }
    
        /** {@inheritDoc} */
        @Override 
        protected void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
    
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                TreeItem<T> treeItem = getTreeItem();
                checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
                setGraphic(checkBox);
                // uninstall bindings
                if (booleanProperty != null) {
                    checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
                }
                if (indeterminateProperty != null) {
                    checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
                }
    
                // install new bindings.
                // this can only handle TreeItems of type CheckBoxTreeItem
                if (treeItem instanceof CheckBoxTreeItem) {
                    CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
                    booleanProperty = cbti.selectedProperty();
                    checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
    
                    indeterminateProperty = cbti.indeterminateProperty();
                    checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
                } else {
                    throw new IllegalStateException("item must be CheckBoxTreeItem");
                }
            }
    
        }
    
        @Override
        protected Skin<?> createDefaultSkin() {
            return new CheckBoxTreeTableRowSkin<>(this);
        }
    
        public static class CheckBoxTreeTableRowSkin<S> extends TreeTableRowSkin<S> {
            protected ObjectProperty<Node> checkGraphic;
    
            /**
             * @param control
             */
            public CheckBoxTreeTableRowSkin(TreeTableRow<S> control) {
                super(control);
            }
    
            /**
             * Note: this is implicitly called from the constructor of LabeledSkinBase.
             * At that time, checkGraphic is not yet instantiated. So we do it here,
             * still having to create it at least twice. That'll be a problem if 
             * anybody would listen to changes ...
             */
            @Override
            protected ObjectProperty<Node> graphicProperty() {
                if (checkGraphic == null) {
                    checkGraphic = new SimpleObjectProperty<Node>(this, "checkGraphic");
                }
                CheckBoxTreeTableRow<S> treeTableRow = getTableRow();
                if (treeTableRow.getTreeItem() == null) {
                    checkGraphic.set(null);   
                } else {
                    checkGraphic.set(treeTableRow.getGraphic());
                }
                return checkGraphic;
            }
    
            protected CheckBoxTreeTableRow<S> getTableRow() {
                return (CheckBoxTreeTableRow<S>) super.getSkinnable();
            }
        }
    
        @SuppressWarnings("unused")
        private static final Logger LOG = Logger
                .getLogger(CheckBoxTreeTableRow.class.getName());
    }
    


    Original answer: hack!

    There's a nutty line of code in it:

    treeItem.setGraphics(checkBox);
    

    That's really whacky, and probably will cause havoc eventually - it's a hack around a layout glitch in TreeTableRowSkin, that for some reason (I couldn't dig up) cannot position a graphic set to the cell. Couldn't make it behave in a custom CheckBoxTreeTableRowSkin that returns the checkBox directly in its graphicProperty() - so here we go with the hack for now.

    /**
     * @author Jeanette Winzenburg, Berlin
     */
    public class CheckBoxTreeTableRowHack<T> extends TreeTableRow<T> {
    
        private CheckBox checkBox;
    
        private ObservableValue<Boolean> booleanProperty;
    
        private BooleanProperty indeterminateProperty;
    
        public CheckBoxTreeTableRowHack() {
            setSelectedStateCallback(item -> {
                if (item instanceof CheckBoxTreeItem<?>) {
                    return ((CheckBoxTreeItem<?>)item).selectedProperty();
                }
                return null;
            });
            this.checkBox = new CheckBox();
            // something weird going on with layout
            checkBox.setAlignment(Pos.TOP_LEFT);
        }
    
        // --- selected state callback property
        private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> 
                selectedStateCallback = 
                new SimpleObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>(
                this, "selectedStateCallback");
    
        /**
         * Property representing the {@link Callback} that is bound to by the 
         * CheckBox shown on screen.
         */
        public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() { 
            return selectedStateCallback; 
        }
    
        /** 
         * Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
         */
        public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) { 
            selectedStateCallbackProperty().set(value); 
        }
    
        /**
         * Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
         */
        public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() { 
            return selectedStateCallbackProperty().get(); 
        }
    
        /** {@inheritDoc} */
        @Override 
        public void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
    
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
    //            
                TreeItem<T> treeItem = getTreeItem();
                // PENDING JW: this is nuts but working ..  certainly will pose problems
                // when re-using the cell
                treeItem.setGraphic(checkBox);
                // this is what CheckBoxTreeCell does, setting the graphic
                // of the tableRow confuses the layout
    //            checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
    //            setGraphic(checkBox);
    
                // uninstall bindings
                if (booleanProperty != null) {
                    checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
                }
                if (indeterminateProperty != null) {
                    checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
                }
    
                // install new bindings.
                // We special case things when the TreeItem is a CheckBoxTreeItem
                if (treeItem instanceof CheckBoxTreeItem) {
                    CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
                    booleanProperty = cbti.selectedProperty();
                    checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
    
                    indeterminateProperty = cbti.indeterminateProperty();
                    checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
                } else {
                    throw new IllegalStateException("item must be CheckBoxTreeItem");
                }
            }
    
        }
    }
    
    // usage: in the example add
    treeTableView.setRowFactory(f -> new CheckBoxTreeTableRowHack<>());