File Tree View Sample

I just wanted to create an application like kind of file explorer to learn about the following contents.

  • 1. How to use directory chooser
  • 2. Tree View with Path
  • 3. Watching for file systems by WatchService
  • 4. Searching files by FileVisitor and PathMatcher
  • 5. How to bind to JavaFX UI with background threads
  • Figure 1

  • code 1(FileTreeViewSample)

This code shows how to use Directory Chooser(Figure 2)

        final DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("select Directory");
        Image image = new Image(getClass().getResourceAsStream("OpenDirectory.png"));
        Button chooserBtn = new Button("", new ImageView(image));
        chooser.setTitle("Select Root Directory");
        chooserBtn.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {
                File selDir = chooser.showDialog(stage);
                if (selDir != null) {
                    rootDirText.setText(selDir.getAbsolutePath());
                }
            }
        });
  • Figure 2(Directory Chooser)

  • code 2-1(PathTreeItem)

This refers to the JavaFX API site[Class ListItem]
But I just wanted to use Path instead of File.
If you use Path for TreeItem it shows absolute paths.(Figure 3)
So I create PathItem class to show file names.(code 2-2)

public class PathTreeItem extends TreeItem<PathItem> {
    private boolean isLeaf = false;
    private boolean isFirstTimeChildren = true;
    private boolean isFirstTimeLeft = true;

    private PathTreeItem(PathItem pathItem) {
        super(pathItem);
    }

    public static TreeItem<PathItem> createNode(PathItem pathItem) {
        return new PathTreeItem(pathItem);
    }

    @Override
    public ObservableList<TreeItem<PathItem>> getChildren() {
        if (isFirstTimeChildren) {
            isFirstTimeChildren = false;
            super.getChildren().setAll(buildChildren(this));
        }
        return super.getChildren();
    }

    @Override
    public boolean isLeaf() {
        if (isFirstTimeLeft) {
            isFirstTimeLeft = false;
            Path path = getValue().getPath();
            isLeaf = !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS);
        }
        return isLeaf;
    }

    private ObservableList<TreeItem<PathItem>> buildChildren(TreeItem<PathItem> treeItem) {
        Path path = treeItem.getValue().getPath();
        if (path != null && Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
            ObservableList<TreeItem<PathItem>> children = FXCollections.observableArrayList();
            try (DirectoryStream<Path> dirs = Files.newDirectoryStream(path)) {
                for (Path dir : dirs) {
                    PathItem pathItem = new PathItem(dir);
                    children.add(createNode(pathItem));
                }
            } catch (IOException ex) {
                Logger.getLogger(FileTreeViewSample.class.getName()).log(Level.SEVERE, null, ex);
            }
            return children;
        }
        return FXCollections.emptyObservableList();
    }
}
  • code 2-2(PathItem class)
public class PathItem {
    private Path path;
    public PathItem(Path path) {
        this.path = path;
    }
    public Path getPath() {
        return path;
    }
    @Override
    public String toString() {
        if (path.getFileName() == null) {
            return path.toString();
        } else {
            return path.getFileName().toString(); // showing file name on the TreeView
        }
    }        
}
  • Figure 3(TreeView with Path)

  • code 3

This refers to the site Watching a Directory for Changes (The Java™ Tutorials > Essential Classes > Basic I/O)

public class WatchTask extends Task<Void>{
    private Path path;
    private StringBuilder message = new StringBuilder();

    public WatchTask(Path path) {
        this.path = path;
    }
    
    @Override
    protected Void call() throws Exception {
        WatchService watcher = FileSystems.getDefault().newWatchService();
        path.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        while (true) {
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException e) {
                break;
            }
            for (WatchEvent<?> event : key.pollEvents()) {
                if (event.kind() == OVERFLOW) {
                    continue;
                }
                Path context = (Path) event.context();
                message.append(context.toAbsolutePath());
                message.append(getKindToMessage(event.kind()));
                message.append(System.getProperty("line.separator"));
                updateMessage(message.toString()); // to bind to the TextArea(see code 5-1)
            }
            boolean valid = key.reset();
            if (!valid) {
                break;
            }
        }
        return null;
    }

    @Override
    protected void cancelled() {
        updateMessage("Watch task was cancelled"); // to bind to the TextArea(see code 5-1)
    }

    private String getKindToMessage(WatchEvent.Kind<?> kind) {
        if (kind == ENTRY_CREATE) {
            return " is created";
        } else if (kind == ENTRY_DELETE) {
            return " is deleted";
        }
        return " is updated";
    }    
}
  • code 4

This code uses FileVisitor and PathMatcher to search files.
you can also use regular expressions, or regex, syntax as well.

public class SearchTask extends Task<Void> {
    private Path path;
    private String pattern;
    private StringProperty resultString;
    
    public SearchTask(Path path, String pattern) {
        this.path = path;
        this.pattern = pattern;
        resultString = new SimpleStringProperty();
    }

    @Override
    protected Void call() throws Exception {
        updateProgress(0, 0);
        updateMessage("searching ...");
        final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            int cnt = 0;
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (matcher.matches(file.getFileName())) {
                    cnt++;
                    resultString.setValue(file.toAbsolutePath().toString()); // to bind to the ListItem(see code5-2)
                    updateMessage(String.format("%d files founded", cnt)); // to bind to the Label(see code5-2)
                    updateProgress(cnt, cnt);  // this does not work
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return null;
    }

    public StringProperty getResultString() {
        return resultString;
    }
}
  • code 5-1

When watch checkbox checked WatchTask starts and shows the changes to the TextArea

public class FileTreeViewSample extends Application {
    .....
        watchChkbox.selectedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal) {
                if (newVal) {
                    watchTask = new WatchTask(rootPath);
                    service.submit(watchTask);
                    watchText.textProperty().bind(watchTask.messageProperty());  // see code-3
                } else {
                    if (watchTask != null && watchTask.isRunning()) {
                        watchTask.cancel();
                        watchText.textProperty().unbind();
                    }
                }
            }
        });
}
  • code 5-2

When search button is clicked the searchTask starts and shows the number of the results to the Label.
The label binding works but I do not know how to bind to the ListView.
So I use StringProperty[searchResult] to get the result from searchTask and add it to the ObservableList.
Once searchTask succeed the ObservableList is added to ListItem.
I think this is not the good way to bind but I do not how to implement ListBinding....

public class FileTreeViewSample extends Application {
    .....
        searchBtn.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {
                searchListItem.clear();                
                searchTask = new SearchTask(rootPath, patternText.getText());
                searchCountLabel.textProperty().bind(searchTask.messageProperty()); // showing the number of files(see code-4)
                searchList.clear();
                searchResult.bind(searchTask.getResultString()); 
                searchTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
                    @Override
                    public void handle(WorkerStateEvent t) {
                        searchListItem.addAll(searchList); // add all search results at once(see code-4)
                    }
                });
                // this bind does not work???
//                searchCountLabel.textProperty().bind(
//                        new StringBinding() {
//                    {
//                        bind(searchTask.progressProperty());
//                    }
//                    @Override
//                    protected String computeValue() {
//                        return String.format("%1.0f files founded", searchTask.progressProperty().getValue());
//                    }
//                });
                service.submit(searchTask);
            }
        });
        // *** binding to search task result one by one ***
        searchResult.addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> ov, String oldVal, String newVal) {
                if (newVal != null) {
                    searchList.add(newVal);
                }
            }
        });
}

The full code is here tomoTaka01/FileTreeViewSample: JavaFX File ... - GitHub
just keep coding...