From 996cd1dfbee494549b9f8f9d7732c1c02bc1da21 Mon Sep 17 00:00:00 2001 From: weisj Date: Mon, 14 Sep 2020 16:50:05 +0200 Subject: [PATCH] Add FileTree component. --- .../darklaf/components/filetree/FileTree.java | 74 +++++ .../filetree/FileTreeCellRenderer.java | 49 +++ .../components/filetree/FileTreeModel.java | 89 +++++ .../components/filetree/FileTreeNode.java | 313 ++++++++++++++++++ .../components/filetree/WatchFileTree.java | 80 +++++ .../filetree/WatchFileTreeModel.java | 187 +++++++++++ core/src/test/java/ui/ComponentDemo.java | 32 +- core/src/test/java/ui/tree/FileTreeDemo.java | 79 +++++ core/src/test/java/util/ClassFinder.java | 4 +- core/src/test/java/util/ResourceWalker.java | 33 +- .../github/weisj/darklaf/util/Lambdas.java | 125 +++++++ 11 files changed, 1025 insertions(+), 40 deletions(-) create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTree.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeCellRenderer.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeModel.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeNode.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTree.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTreeModel.java create mode 100644 core/src/test/java/ui/tree/FileTreeDemo.java create mode 100644 utils/src/main/java/com/github/weisj/darklaf/util/Lambdas.java diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTree.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTree.java new file mode 100644 index 00000000..0e43578b --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTree.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.io.File; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import javax.swing.tree.TreeModel; + +public class FileTree extends JTree { + + public FileTree() { + this(null); + } + + public FileTree(final File rootFile) { + this(rootFile, false); + } + + public FileTree(final File rootFile, final boolean showHiddenFiles) { + FileSystemView fileSystemView = FileSystemView.getFileSystemView(); + setFileTreeModel(createModel(fileSystemView, rootFile, showHiddenFiles)); + setCellRenderer(new FileTreeCellRenderer(fileSystemView)); + setRootVisible(false); + } + + protected FileTreeModel createModel(final FileSystemView fsv, final File rootFile, final boolean showHiddenFiles) { + return new FileTreeModel(fsv, rootFile, showHiddenFiles); + } + + public boolean isShowHiddenFiles() { + return getModel().isShowHiddenFiles(); + } + + public void setShowHiddenFiles(final boolean showHiddenFiles) { + getModel().setShowHiddenFiles(showHiddenFiles); + } + + @Override + public FileTreeModel getModel() { + return (FileTreeModel) super.getModel(); + } + + public void setFileTreeModel(final FileTreeModel fileTreeModel) { + super.setModel(fileTreeModel); + } + + @Override + public void setModel(final TreeModel newModel) {} + + public void reload() { + getModel().reload(); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeCellRenderer.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeCellRenderer.java new file mode 100644 index 00000000..94fee486 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeCellRenderer.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.awt.*; +import java.nio.file.Path; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import javax.swing.tree.DefaultTreeCellRenderer; + +public class FileTreeCellRenderer extends DefaultTreeCellRenderer { + + private final FileSystemView fsv; + + public FileTreeCellRenderer(final FileSystemView fileSystemView) { + this.fsv = fileSystemView; + } + + @Override + public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean selected, + final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { + Path f = ((FileTreeNode) value).getFile(); + if (f != null) { + setIcon(fsv.getSystemIcon(f.toFile())); + setText(fsv.getSystemDisplayName(f.toFile())); + } + return this; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeModel.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeModel.java new file mode 100644 index 00000000..00faf914 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeModel.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.io.File; +import java.nio.file.Path; + +import javax.swing.filechooser.FileSystemView; +import javax.swing.tree.DefaultTreeModel; + +public class FileTreeModel extends DefaultTreeModel { + + protected final FileSystemView fsv; + protected boolean showHiddenFiles; + + public FileTreeModel(final FileSystemView fileSystemView) { + this(fileSystemView, (Path) null, false); + } + + public FileTreeModel(final FileSystemView fileSystemView, final File root, final boolean showHiddenFiles) { + this(fileSystemView, root != null ? root.toPath() : null, showHiddenFiles); + } + + public FileTreeModel(final FileSystemView fileSystemView, final Path root, final boolean showHiddenFiles) { + super(null); + init(); + this.showHiddenFiles = showHiddenFiles; + this.fsv = fileSystemView; + this.root = createRoot(root); + } + + protected void init() {} + + protected FileTreeNode createRoot(final Path root) { + if (root == null) { + return new FileTreeNode.RootNode(this); + } else { + return createNode(null, root); + } + } + + @Override + public void reload() { + getRoot().reload(); + } + + @Override + public FileTreeNode getRoot() { + return (FileTreeNode) super.getRoot(); + } + + public void setShowHiddenFiles(final boolean showHiddenFiles) { + if (showHiddenFiles == this.showHiddenFiles) + return; + this.showHiddenFiles = showHiddenFiles; + reload(); + } + + public boolean isShowHiddenFiles() { + return showHiddenFiles; + } + + protected FileTreeNode createNode(final FileTreeNode parent, final Path file) { + return new FileTreeNode(parent, file, this); + } + + protected void register(final FileTreeNode node) {} + + protected void unregister(final FileTreeNode node) {} +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeNode.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeNode.java new file mode 100644 index 00000000..273ecaa8 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/FileTreeNode.java @@ -0,0 +1,313 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.swing.*; +import javax.swing.tree.TreeNode; + +public class FileTreeNode implements TreeNode, Comparable { + + protected final FileTreeNode parent; + protected final FileTreeModel model; + protected final Path file; + protected AtomicReference> children; + protected WatchKey watchKey; + + public FileTreeNode(final FileTreeNode parent, final Path file, final FileTreeModel model) { + this.model = model; + this.file = file; + this.parent = parent; + this.children = new AtomicReference<>(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + FileTreeNode that = (FileTreeNode) o; + return Objects.equals(file, that.file); + } + + @Override + public int hashCode() { + return file != null ? file.hashCode() : 0; + } + + public Path getFile() { + return file; + } + + private int addSorted(final List nodes, final FileTreeNode node) { + int index = Collections.binarySearch(nodes, node); + if (index < 0) + index = ~index; + nodes.add(index, node); + model.register(node); + return index; + } + + private void add(final List nodes, final FileTreeNode node) { + int index = addSorted(nodes, node); + model.nodesWereInserted(FileTreeNode.this, new int[] {index}); + } + + private void remove(final List nodes, final FileTreeNode node) { + int index = nodes.indexOf(node); + if (index < 0) + return; + nodes.remove(node); + model.unregister(node); + model.nodesWereRemoved(FileTreeNode.this, new int[] {index}, new Object[] {node}); + } + + public void reload() { + reload(Integer.MAX_VALUE); + } + + protected void reload(final int depth) { + if (depth < 0) + return; + if (children.get() == null) + return; + List fileList = children.get(); + doInBackground((Consumer proc) -> { + traverseChildren(this::toNodes).accept(proc); + fileList.stream().filter(n -> Files.notExists(n.file)).forEach(proc); + }, chunks -> { + for (FileTreeNode node : chunks) { + if (Files.notExists(node.file)) { + remove(fileList, node); + } else { + if (model.showHiddenFiles) { + if (!fileList.contains(node)) { + add(fileList, node); + } + } else { + if (isHidden(node.file)) { + remove(fileList, node); + } else if (!fileList.contains(node)) { + add(fileList, node); + } + } + } + } + }, () -> { + if (depth > 0) + fileList.forEach(n -> n.reload(depth - 1)); + }); + } + + private List getChildren() { + return children.updateAndGet(list -> { + if (list != null) { + return list; + } + List fileList = Collections.synchronizedList(new ArrayList<>()); + doInBackground( + traverseChildren(asNodes(stream -> stream.filter(p -> model.showHiddenFiles || !isHidden(p)))), + chunks -> { + Collections.sort(fileList); + int[] indices = new int[chunks.size()]; + int i = 0; + for (FileTreeNode node : chunks) { + indices[i] = addSorted(fileList, node); + i++; + } + model.nodesWereInserted(FileTreeNode.this, indices); + }); + return fileList; + }); + } + + private boolean isHidden(final Path p) { + try { + return Files.isHidden(p) || p.toFile().isHidden(); + } catch (IOException e) { + return false; + } + } + + private Function, Stream> asNodes( + final Function, Stream> transformer) { + return stream -> toNodes(transformer.apply(stream)); + } + + private Stream toNodes(final Stream stream) { + return stream.map(p -> model.createNode(FileTreeNode.this, p)); + } + + private Consumer> traverseChildren(final Function, Stream> transformer) { + return publish -> { + if (Files.isDirectory(file)) { + try (Stream files = Files.walk(file, 1, FileVisitOption.FOLLOW_LINKS)) { + transformer.apply(files.filter(p -> !file.equals(p)).filter(Files::isReadable)).forEach(publish); + } catch (IOException ignored) { + } + } + }; + } + + private void doInBackground(final Consumer> task, final Consumer> processor) { + doInBackground(task, processor, () -> { + }); + } + + private void doInBackground(final Consumer> task, final Consumer> processor, + final Runnable doneTask) { + SwingWorker worker = new SwingWorker() { + @Override + public Void doInBackground() { + task.accept(this::publish); + return null; + } + + @Override + protected void process(final List chunks) { + processor.accept(chunks); + } + + @Override + protected void done() { + doneTask.run(); + } + }; + worker.execute(); + } + + @Override + public TreeNode getChildAt(final int childIndex) { + return getChildren().get(childIndex); + } + + @Override + public int getChildCount() { + return getChildren().size(); + } + + @Override + public TreeNode getParent() { + return parent; + } + + @Override + public int getIndex(final TreeNode node) { + if (node == null) { + throw new IllegalArgumentException("argument is null"); + } + if (!isNodeChild(node)) { + return -1; + } + return getChildren().indexOf(node); + } + + @Override + public boolean getAllowsChildren() { + return Files.isDirectory(file); + } + + @Override + public boolean isLeaf() { + return getChildren().size() == 0; + } + + @Override + public Enumeration children() { + return Collections.enumeration(children.get()); + } + + public boolean isNodeChild(final TreeNode aNode) { + if (aNode == null) + return false; + return (aNode.getParent() == this); + } + + @Override + public int compareTo(final FileTreeNode o) { + if (o == null) + return -1; + boolean thisDir = Files.isDirectory(file); + boolean oDir = Files.isDirectory(o.file); + if (thisDir == oDir) { + return file.compareTo(o.file); + } else { + return thisDir ? -1 : 1; + } + } + + public static class RootNode extends FileTreeNode { + + public RootNode(final FileTreeModel model) { + super(null, null, model); + List nodes = new ArrayList<>(); + FileSystems.getDefault().getRootDirectories().forEach(p -> { + FileTreeNode node = model.createNode(RootNode.this, p); + model.register(node); + nodes.add(node); + }); + children.set(nodes); + } + + @Override + protected void reload(final int depth) { + if (depth < 0) + return; + List nodes = children.get(); + FileSystems.getDefault().getRootDirectories().forEach(p -> { + FileTreeNode node = model.createNode(this, p); + if (!nodes.contains(node)) { + model.register(node); + nodes.add(node); + } + }); + nodes.removeIf(n -> { + if (Files.notExists(n.file)) { + model.unregister(n); + return true; + } + return false; + }); + + if (depth > 0) + children.get().forEach(n -> n.reload(depth - 1)); + } + + @Override + public boolean isLeaf() { + return false; + } + + @Override + public boolean getAllowsChildren() { + return true; + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTree.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTree.java new file mode 100644 index 00000000..411199a1 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTree.java @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.io.File; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; + +public class WatchFileTree extends FileTree { + + public WatchFileTree() { + super(null); + } + + public WatchFileTree(final File rootFile) { + super(rootFile, false); + } + + public WatchFileTree(final File rootFile, final boolean showHiddenFiles) { + super(rootFile, showHiddenFiles); + } + + @Override + protected FileTreeModel createModel(final FileSystemView fsv, final File rootFile, final boolean showHiddenFiles) { + return new WatchFileTreeModel(fsv, rootFile, showHiddenFiles); + } + + @Override + public void setFileTreeModel(final FileTreeModel fileTreeModel) { + if (getModel() == fileTreeModel) + return; + if (getModel() instanceof WatchFileTreeModel) { + ((WatchFileTreeModel) getModel()).stopWatching(); + } + if (isVisible() && fileTreeModel instanceof WatchFileTreeModel) { + ((WatchFileTreeModel) fileTreeModel).startWatching(); + } + super.setFileTreeModel(fileTreeModel); + } + + @Override + public void addNotify() { + super.addNotify(); + SwingUtilities.invokeLater(() -> { + if (getModel() instanceof WatchFileTreeModel) { + ((WatchFileTreeModel) getModel()).startWatching(); + } + }); + } + + @Override + public void removeNotify() { + super.removeNotify(); + SwingUtilities.invokeLater(() -> { + if (getModel() instanceof WatchFileTreeModel) { + ((WatchFileTreeModel) getModel()).stopWatching(); + } + }); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTreeModel.java b/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTreeModel.java new file mode 100644 index 00000000..dfb677e4 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/filetree/WatchFileTreeModel.java @@ -0,0 +1,187 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.filetree; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +import javax.swing.filechooser.FileSystemView; + +import com.github.weisj.darklaf.util.LogUtil; + +public class WatchFileTreeModel extends FileTreeModel { + + private static final Logger LOGGER = LogUtil.getLogger(WatchFileTreeModel.class); + private static final ScheduledExecutorService scheduler = createScheduler(); + private WatchService watchService; + private Map nodeMap; + private Object lock; + + private final AtomicBoolean isScheduled = new AtomicBoolean(false); + private ScheduledFuture watchTask; + + private static WatchService createWatchService() { + WatchService ws = null; + try { + ws = FileSystems.getDefault().newWatchService(); + } catch (IOException e) { + e.printStackTrace(); + } + return ws; + } + + private static ScheduledExecutorService createScheduler() { + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, r -> { + final Thread thread = new Thread(r, "File Tree Watch Thread"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + return executor; + } + + public WatchFileTreeModel(final FileSystemView fileSystemView) { + super(fileSystemView); + } + + public WatchFileTreeModel(final FileSystemView fileSystemView, final File root, final boolean showHiddenFiles) { + super(fileSystemView, root, showHiddenFiles); + } + + public WatchFileTreeModel(final FileSystemView fileSystemView, final Path root, final boolean showHiddenFiles) { + super(fileSystemView, root, showHiddenFiles); + } + + @Override + protected void init() { + lock = new Object(); + watchService = createWatchService(); + nodeMap = Collections.synchronizedMap(new HashMap<>()); + } + + private Object getLock() { + return lock; + } + + protected WatchService getWatchService() { + return watchService; + } + + protected Map getNodeMap() { + return nodeMap; + } + + public void startWatching() { + if (watchTask != null) + return; + isScheduled.set(true); + watchTask = scheduler.schedule(this::watch, 0, TimeUnit.SECONDS); + } + + public void stopWatching() { + if (watchTask != null) { + isScheduled.set(false); + watchTask.cancel(true); + watchTask = null; + } + } + + private void watch() { + while (isScheduled.get()) { + WatchKey key; + try { + key = watchService.take(); + } catch (InterruptedException x) { + x.printStackTrace(); + return; + } + + FileTreeNode parent = getNodeMap().get(key.watchable()); + if (parent != null) { + LOGGER.fine(() -> "Event for \"" + parent.file + "\""); + if (parent.parent != null) { + parent.parent.reload(1); + } else { + parent.reload(0); + } + } + + List> watchEventList = key.pollEvents(); + for (WatchEvent event : watchEventList) { + WatchEvent.Kind kind = event.kind(); + + Path path = (Path) event.context(); + if (kind == StandardWatchEventKinds.OVERFLOW) { + continue; + } + LOGGER.finer("Event Type " + kind.name()); + FileTreeNode node = getNodeMap().get(((Path) key.watchable()).resolve(path)); + if (node != null) { + LOGGER.finer(() -> "Affected node \"" + node.file + "\""); + node.reload(0); + } + } + + key.reset(); + } + } + + protected void register(final FileTreeNode node) { + synchronized (getLock()) { + WatchService ws = getWatchService(); + if (ws == null || !Files.isDirectory(node.file)) + return; + if (getNodeMap().containsKey(node.file)) + return; + try { + LOGGER.finer(() -> "Register watch service for \"" + node.file + "\""); + node.watchKey = node.file.register(ws, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); + getNodeMap().put(node.file, node); + } catch (IOException ignored) { + } + } + } + + protected void unregister(final FileTreeNode node) { + synchronized (getLock()) { + if (node.watchKey == null) + return; + LOGGER.finer(() -> "Unregister watch service for \"" + node.file + "\""); + getNodeMap().remove(node.file); + node.watchKey.cancel(); + } + } +} diff --git a/core/src/test/java/ui/ComponentDemo.java b/core/src/test/java/ui/ComponentDemo.java index 7f3ca1bb..b64214d5 100644 --- a/core/src/test/java/ui/ComponentDemo.java +++ b/core/src/test/java/ui/ComponentDemo.java @@ -66,8 +66,12 @@ public interface ComponentDemo { LafManager.setLogLevel(Level.FINE); System.setProperty("apple.laf.useScreenMenuBar", "true"); SwingUtilities.invokeLater(() -> { - if (!LafManager.isInstalled()) { - LafManager.install(demo.createTheme()); + if (demo.useDarkLaf()) { + if (!LafManager.isInstalled()) { + LafManager.install(demo.createTheme()); + } + } else { + installSystemLaf(); } Window window; if (!asDialog) { @@ -110,6 +114,10 @@ public interface ComponentDemo { }); } + default boolean useDarkLaf() { + return true; + } + default Dimension getDisplayDimension() { return null; } @@ -234,18 +242,12 @@ public interface ComponentDemo { }); dev.add(new JCheckBoxMenuItem("Darklaf/System Laf") { { - setSelected(true); + setSelected(LafManager.isInstalled()); addActionListener(e -> { if (isSelected()) { LafManager.install(); } else { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - LafManager.updateLaf(); - } catch (ClassNotFoundException | UnsupportedLookAndFeelException | IllegalAccessException - | InstantiationException classNotFoundException) { - classNotFoundException.printStackTrace(); - } + installSystemLaf(); } }); } @@ -253,5 +255,15 @@ public interface ComponentDemo { return dev; } + static void installSystemLaf() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + LafManager.updateLaf(); + } catch (ClassNotFoundException | UnsupportedLookAndFeelException | IllegalAccessException + | InstantiationException classNotFoundException) { + classNotFoundException.printStackTrace(); + } + } + String getTitle(); } diff --git a/core/src/test/java/ui/tree/FileTreeDemo.java b/core/src/test/java/ui/tree/FileTreeDemo.java new file mode 100644 index 00000000..00f97a1b --- /dev/null +++ b/core/src/test/java/ui/tree/FileTreeDemo.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package ui.tree; + +import java.awt.*; + +import javax.swing.*; + +import ui.ComponentDemo; +import ui.DemoPanel; + +import com.github.weisj.darklaf.components.OverlayScrollPane; +import com.github.weisj.darklaf.components.filetree.FileTree; +import com.github.weisj.darklaf.components.filetree.WatchFileTree; +import com.github.weisj.darklaf.ui.button.ButtonConstants; +import com.github.weisj.darklaf.ui.tree.DarkTreeUI; + +public class FileTreeDemo implements ComponentDemo { + + public static void main(final String[] args) { + ComponentDemo.showDemo(new FileTreeDemo()); + } + + @Override + public JComponent createComponent() { + FileTree tree = new WatchFileTree(null, false); + DemoPanel panel = new DemoPanel(new OverlayScrollPane(tree), new BorderLayout(), 0); + JPanel controlPanel = panel.addControls(); + controlPanel.add(new JCheckBox("Show hidden files") { + { + setSelected(tree.isShowHiddenFiles()); + addActionListener(e -> tree.setShowHiddenFiles(isSelected())); + } + }); + controlPanel.add(new JButton("Reload") { + { + putClientProperty(ButtonConstants.KEY_THIN, true); + addActionListener(e -> tree.reload()); + } + }); + controlPanel = panel.addControls(); + controlPanel.add(new JLabel(DarkTreeUI.KEY_LINE_STYLE + ":", JLabel.RIGHT)); + controlPanel.add(new JComboBox() { + { + addItem(DarkTreeUI.STYLE_LINE); + addItem(DarkTreeUI.STYLE_DASHED); + addItem(DarkTreeUI.STYLE_NONE); + setSelectedItem(DarkTreeUI.STYLE_LINE); + addItemListener(e -> tree.putClientProperty(DarkTreeUI.KEY_LINE_STYLE, e.getItem())); + } + }); + return panel; + } + + @Override + public String getTitle() { + return "File Tree Demo"; + } + +} diff --git a/core/src/test/java/util/ClassFinder.java b/core/src/test/java/util/ClassFinder.java index 885b09e7..0071dbb5 100644 --- a/core/src/test/java/util/ClassFinder.java +++ b/core/src/test/java/util/ClassFinder.java @@ -27,12 +27,14 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import com.github.weisj.darklaf.util.Lambdas; + public class ClassFinder { public static List getInstancesOfType(final Class type, final String... packages) { try (ResourceWalker walker = ResourceWalker.walkResources(packages)) { return walker.stream().filter(p -> p.endsWith(".class")).map(p -> p.replace('/', '.')) - .map(p -> p.substring(0, p.length() - 6)).map(ResourceWalker.orDefault(Class::forName, null)) + .map(p -> p.substring(0, p.length() - 6)).map(Lambdas.orDefault(Class::forName, null)) .filter(Objects::nonNull).filter(type::isAssignableFrom).filter(cls -> !cls.isInterface()) .filter(cls -> !Modifier.isAbstract(cls.getModifiers())).map(cls -> (Class) cls) .map(ClassFinder::getInstance).filter(Objects::nonNull).map(type::cast) diff --git a/core/src/test/java/util/ResourceWalker.java b/core/src/test/java/util/ResourceWalker.java index 1ed6bd4d..abcaab34 100644 --- a/core/src/test/java/util/ResourceWalker.java +++ b/core/src/test/java/util/ResourceWalker.java @@ -29,10 +29,11 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import com.github.weisj.darklaf.util.Lambdas; + public class ResourceWalker implements AutoCloseable { private final List fileSystemList = new ArrayList<>(); @@ -74,8 +75,8 @@ public class ResourceWalker implements AutoCloseable { String pathName = pack; ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Stream stream = enumerationAsStream( - orDefault(classLoader::getResources, Collections.emptyEnumeration()).apply(pathName)); - return stream.map(wrap(URL::toURI)).flatMap(uri -> { + Lambdas.orDefault(classLoader::getResources, Collections.emptyEnumeration()).apply(pathName)); + return stream.map(Lambdas.wrap(URL::toURI)).flatMap(uri -> { if ("jar".equals(uri.getScheme())) { try { FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); @@ -114,30 +115,4 @@ public class ResourceWalker implements AutoCloseable { files = new File[0]; return Arrays.stream(files).flatMap(this::walkFolder); } - - public static Function orDefault(final CheckedFunction wrappee, - final K fallback) { - return t -> { - try { - return wrappee.apply(t); - } catch (final Throwable e) { - return fallback; - } - }; - } - - public static Function wrap(final CheckedFunction wrappee) { - return t -> { - try { - return wrappee.apply(t); - } catch (final Throwable e) { - throw new RuntimeException(e); - } - }; - } - - public interface CheckedFunction { - - K apply(final T value) throws E; - } } diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/Lambdas.java b/utils/src/main/java/com/github/weisj/darklaf/util/Lambdas.java new file mode 100644 index 00000000..0dbeb79d --- /dev/null +++ b/utils/src/main/java/com/github/weisj/darklaf/util/Lambdas.java @@ -0,0 +1,125 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.util; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class Lambdas { + + private Lambdas() {} + + public static Function orDefault(final CheckedFunction wrappee, + final K fallback) { + return t -> { + try { + return wrappee.apply(t); + } catch (final Throwable e) { + return fallback; + } + }; + } + + public static Supplier orDefault(final CheckedSupplier wrappee, + final T fallback) { + return () -> { + try { + return wrappee.get(); + } catch (final Throwable e) { + return fallback; + } + }; + } + + public static Predicate orDefault(final CheckedPredicate wrappee, + final boolean fallback) { + return t -> { + try { + return wrappee.test(t); + } catch (final Throwable e) { + return fallback; + } + }; + } + + public static Function wrap(final CheckedFunction wrappee) { + return t -> { + try { + return wrappee.apply(t); + } catch (final Throwable e) { + throw new RuntimeException(e); + } + }; + } + + public static Consumer wrap(final CheckedConsumer wrappee) { + return t -> { + try { + wrappee.accept(t); + } catch (final Throwable e) { + throw new RuntimeException(e); + } + }; + } + + public static Supplier wrap(final CheckedSupplier wrappee) { + return () -> { + try { + return wrappee.get(); + } catch (final Throwable e) { + throw new RuntimeException(e); + } + }; + } + + public static Predicate wrap(final CheckedPredicate wrappee) { + return t -> { + try { + return wrappee.test(t); + } catch (final Throwable e) { + throw new RuntimeException(e); + } + }; + } + + public interface CheckedFunction { + + K apply(final T value) throws E; + } + + public interface CheckedConsumer { + + void accept(final T value) throws E; + } + + public interface CheckedSupplier { + + T get() throws E; + } + + public interface CheckedPredicate { + + boolean test(final T value) throws E; + } +}