stephan
stephan

Reputation: 271

JavaFx CheckBoxTreeItem<Path> selecting root item error

I'm trying to develop a file copy application. I've created a checkbox tree item with current directories on file system.

But when I select first node (c:/ directory), it takes a long time. How can I select all directories easily and quickly?

Here is my first FXML load class:

@Override
public void initialize(URL location, ResourceBundle resources) {

    TreeView pathTree = new MyFileTreeView().getMyFilePathTree();
    vBoxFileTree.getChildren().add(pathTree);
}

This is my treeView component:

public class MyFileTreeView {

    private TreeView<Path> filePathTree;
    private List<Path> rootDirectories;
    private Logger logger = Logger.getLogger(MyFileTreeView.class);

    public MyFileTreeView() {

        rootDirectories = new ArrayList<>();

        Iterable<Path> roots = FileSystems.getDefault().getRootDirectories();
        for (Path root : roots) {
            rootDirectories.add(root);
        }
    }

    public TreeView getMyFilePathTree() {

        if (filePathTree == null) {

            filePathTree = new TreeView<>(getRootItem());
            filePathTree.setPrefHeight(600.0d);
            filePathTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
            filePathTree.setCellFactory((TreeView<Path> t) -> new TreeCellImpl());
            filePathTree.setShowRoot(false);
        }

        return filePathTree;
    }

    private TreeItem getRootItem() {

        TreeItem rootItem = new TreeItem();

        for (Path path : rootDirectories) {
            MyFileTreeItem item = new MyFileTreeItem(path);
            item.setIndependent(false);
            rootItem.getChildren().add(item);
            logger.info(path.toString() + " directory has been added to fileTree!");
        }

        return rootItem;
    }
}

And this is tree item:

public class MyFileTreeItem extends CheckBoxTreeItem<Path> {

    private boolean isLeaf;
    private boolean isFirstTimeChildren = true;
    private boolean isFirstTimeLeaf = true;

    public MyFileTreeItem(Path path) {
        super(path);
    }

    @Override
    public boolean isLeaf() {

        if (isFirstTimeLeaf) {

            isFirstTimeLeaf = false;
            Path path = getValue();
            isLeaf = Files.isRegularFile(path);
        }
        return isLeaf;
    }

    @Override
    public ObservableList<TreeItem<Path>> getChildren() {

        if (isFirstTimeChildren) {
            isFirstTimeChildren = false;
            super.getChildren().setAll(buildChildren(this));
        }

        return super.getChildren();
    }

    private ObservableList<TreeItem<Path>> buildChildren(CheckBoxTreeItem<Path> treeItem) {

        Path path = treeItem.getValue();
        if ((path != null) && (Files.isDirectory(path))) {

            try (Stream<Path> pathStream = Files.list(path)) {
                return pathStream
                        .map(p -> new MyFileTreeItem(p))
                        .collect(Collectors.toCollection(() ->
                                FXCollections.observableArrayList()));
            } catch (IOException e) {
            }
        }

        return FXCollections.emptyObservableList();
    }
}

error image

UPDATE:

I added indeterminate property thanks to @fabian

public class FileTreeItem extends TreeItem<Path> {

    private boolean isLeaf;
    private boolean isFirstTimeChildren = true;
    private boolean isFirstTimeLeaf = true;

    private BooleanProperty selected;
    private BooleanProperty indeterminate;

    public FileTreeItem(Path path) {
        this(path, false, false);
    }

    protected FileTreeItem(Path path, boolean selected, boolean indeterminate) {

        super(path);
        this.selected = new SimpleBooleanProperty(selected);
        this.indeterminate = new SimpleBooleanProperty(indeterminate);
        this.selected.addListener((o, oldValue, newValue) -> {

            if (!isLeaf() && !isFirstTimeChildren) {

                if (!isIndeterminate()) {
                    for (TreeItem<Path> ti : getChildren()) {
                        ((FileTreeItem) ti).setSelected(newValue);
                    }
                }

                if (isIndeterminate() && newValue) {
                    setIndeterminate(false);
                    for (TreeItem<Path> ti : getChildren()) {
                        ((FileTreeItem) ti).setSelected(newValue);
                    }
                }
            }

            if (!newValue) {

                if (getParent() instanceof FileTreeItem) {
                    FileTreeItem parent = (FileTreeItem) getParent();
                    parent.setIndeterminate(true);
                    parent.setSelected(false);
                }

            } else {

                if (getParent() instanceof FileTreeItem) {

                    boolean allChildSelected = true;
                    FileTreeItem parent = (FileTreeItem) getParent();
                    for (TreeItem<Path> child : parent.getChildren()) {
                        if (!((FileTreeItem) child).isSelected()) {
                            allChildSelected = false;
                            break;
                        }
                    }

                    if (allChildSelected && !parent.isSelected()) {
                        setIndeterminate(false);
                        parent.setIndeterminate(false);
                        parent.setSelected(true);
                    }
                }
            }
        });
    }

    @Override
    public boolean isLeaf() {

        if (isFirstTimeLeaf) {
            isFirstTimeLeaf = false;
            Path path = getValue();
            isLeaf = Files.isRegularFile(path);
        }
        return isLeaf;
    }

    @Override
    public ObservableList<TreeItem<Path>> getChildren() {

        if (isFirstTimeChildren) {
            isFirstTimeChildren = false;
            super.getChildren().setAll(buildChildren(this));
        }

        return super.getChildren();
    }

    private List<TreeItem<Path>> buildChildren(FileTreeItem treeItem) {

        Path path = treeItem.getValue();
        if ((path != null) && (Files.isDirectory(path))) {

            final boolean select = treeItem.isSelected();
            boolean indeterminate = treeItem.isIndeterminate();

            try (Stream<Path> pathStream = Files.list(path)) {

                List<TreeItem<Path>> res = new ArrayList<>();
                pathStream
                        .map(p -> new FileTreeItem(p, select, indeterminate))
                        .forEach(res::add);
                return res;
            } catch (IOException e) {
            }
        }

        return Collections.emptyList();
    }

    public boolean isSelected() {
        return selected.get();
    }

    public BooleanProperty selectedProperty() {
        return selected;
    }

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

    public boolean isIndeterminate() {
        return indeterminate.get();
    }

    public BooleanProperty indeterminateProperty() {
        return indeterminate;
    }

    public void setIndeterminate(boolean indeterminate) {
        this.indeterminate.set(indeterminate);
    }
}

Upvotes: 0

Views: 622

Answers (1)

fabian
fabian

Reputation: 82531

When you change the selected state of a CheckBoxTreeItem the state of child items is set to the same value. This means the getChildren method is called and for all children the selected property is set too. This way you effectively do a depth first traversal of all the contents of a directory.

You need directly extend TreeItem for this reason and implement the required properties. You need to make sure when the selected property is updated, you iterate through the child items only if getChildren has already been called:

public class FileTreeItem extends TreeItem<Path> {

    private boolean isLeaf;
    private boolean isFirstTimeChildren = true;
    private boolean isFirstTimeLeaf = true;

    private final BooleanProperty selected;
    private final BooleanProperty indeterminate;

    protected FileTreeItem(Path path, boolean selected) {
        super(path);
        this.indeterminate = new SimpleBooleanProperty();
        this.selected = new SimpleBooleanProperty(selected);
        this.selected.addListener((o, oldValue, newValue) -> {
            if (!updating) {
                if (!isLeaf() && !isFirstTimeChildren) {
                    // propagate selection to children if they were created yet
                    for (TreeItem<Path> ti : getChildren()) {
                        FileTreeItem fti = (FileTreeItem) ti;
                        fti.setSelected(newValue);
                    }
                }

                // update ancestors
                TreeItem<Path> parent = getParent();
                while ((parent instanceof FileTreeItem)
                        && updateAncestorState((FileTreeItem) parent)) {
                    parent = parent.getParent();
                }
            }
        });
    }

    /**
     * flag preventing circular calls during update.
     */
    private boolean updating;

    protected static boolean updateAncestorState(FileTreeItem item) {
        List<TreeItem<Path>> children = item.getChildren();

        boolean hasUnselected = false;
        boolean hasSelected = false;
        for (Iterator<TreeItem<Path>> it = children.iterator();!(hasSelected && hasUnselected) && it.hasNext();) {
            TreeItem<Path> ti = it.next();
            FileTreeItem child = (FileTreeItem) ti;
            if (child.isSelected()) {
                hasSelected = true;
            } else {
                hasUnselected = true;
                if (child.isIndeterminate()) {
                    hasSelected = true;
                }
            }
        }

        item.updating = true;
        boolean changed = false;

        if (hasUnselected) {
            if (item.isSelected() || item.isIndeterminate() != hasSelected) {
                changed = true;
                item.setSelected(false);
                item.setIndeterminate(hasSelected);
            }
        } else {
            if (!item.isSelected()) {
                changed = true;
                item.setSelected(true);
            }
            item.setIndeterminate(false);
        }
        item.updating = false;

        return changed;
    }

    public FileTreeItem(Path path) {
        this(path, false);
    }

    @Override
    public boolean isLeaf() {
        if (isFirstTimeLeaf) {
            isFirstTimeLeaf = false;
            Path path = getValue();
            isLeaf = Files.isRegularFile(path);
        }
        return isLeaf;
    }

    @Override
    public ObservableList<TreeItem<Path>> getChildren() {

        if (isFirstTimeChildren) {
            isFirstTimeChildren = false;
            super.getChildren().setAll(buildChildren(this));
        }

        return super.getChildren();
    }

    private List<TreeItem<Path>> buildChildren(FileTreeItem treeItem) {
        Path path = treeItem.getValue();
        if ((path != null) && (Files.isDirectory(path))) {
            final boolean select = treeItem.isSelected();
            try (Stream<Path> pathStream = Files.list(path)) {
                List<TreeItem<Path>> res = new ArrayList<>();
                pathStream
                        .map(p -> new FileTreeItem(p, select))
                        .forEach(res::add);
                return res;
            } catch (IOException e) {
            }
        }

        return Collections.emptyList();
    }

    /* methods for selected & indeterminate properties */
}

Edit

Displaying the indeterminate property requires you to implement your own TreeCell:

public class FileItemCheckBoxTreeCell extends TreeCell<Path> {

    private BooleanProperty oldSelectedProperty;
    private BooleanProperty oldIndeterminateProperty;

    private final CheckBox checkBox;
    private final StringConverter<TreeItem<Path>> converter;

    public FileItemCheckBoxTreeCell(StringConverter<TreeItem<Path>> converter) {
        if (converter == null) {
            throw new IllegalArgumentException();
        }
        this.converter = converter;
        this.checkBox = new CheckBox();
    }

    @Override
    protected void updateItem(Path item, boolean empty) {
        // clear old binding
        if (oldSelectedProperty != null) {
            checkBox.selectedProperty().unbindBidirectional(oldSelectedProperty);
            checkBox.indeterminateProperty().unbindBidirectional(oldIndeterminateProperty);
            oldSelectedProperty = null;
            oldIndeterminateProperty = null;
        }
        checkBox.indeterminateProperty().unbind();

        super.updateItem(item, empty);

        if (empty) {
            setGraphic(null);
            setText("");
        } else {
            TreeItem<Path> treeItem = getTreeItem();
            setText(converter.toString(treeItem));
            if (treeItem instanceof FileTreeItem) {
                setGraphic(checkBox);
                FileTreeItem fti = (FileTreeItem) treeItem;

                oldSelectedProperty = fti.selectedProperty();
                oldIndeterminateProperty = fti.indeterminateProperty();

                checkBox.selectedProperty().bindBidirectional(oldSelectedProperty);
                checkBox.indeterminateProperty().bindBidirectional(oldIndeterminateProperty);
            } else {
                setGraphic(null);
            }
        }
    }

    public static Callback<TreeView<Path>, TreeCell<Path>> forTreeView(StringConverter<TreeItem<Path>> converter) {
        if (converter == null) {
            throw new IllegalArgumentException();
        }
        return tv -> new FileItemCheckBoxTreeCell(converter);
    }

}
public TreeView getMyFilePathTree() {

    if (filePathTree == null) {

        filePathTree = new TreeView<>(getRootItem());
        filePathTree.setPrefHeight(600.0d);
         filePathTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        // tell cell factory to use the selected property for checkbox
        filePathTree.setCellFactory(FileItemCheckBoxTreeCell.forTreeView(new StringConverter<TreeItem<Path>>() {

            @Override
            public String toString(TreeItem<Path> object) {
                if (object == null) {
                    return "";
                }
                Path p = object.getValue();
                if (p == null) {
                    return "";
                }
                p = p.getFileName();
                return p == null ? object.getValue().toString() : p.toString();
            }

            @Override
            public TreeItem<Path> fromString(String string) {
                throw new UnsupportedOperationException();
            }

        }));
        filePathTree.setShowRoot(false);
    }

    return filePathTree;
}

Upvotes: 2

Related Questions