Reputation: 271
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();
}
}
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
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