diff --git a/core/src/main/java/com/github/weisj/darklaf/components/RotatableIconAnimator.java b/core/src/main/java/com/github/weisj/darklaf/components/RotatableIconAnimator.java index 1899d359..151142d5 100644 --- a/core/src/main/java/com/github/weisj/darklaf/components/RotatableIconAnimator.java +++ b/core/src/main/java/com/github/weisj/darklaf/components/RotatableIconAnimator.java @@ -27,6 +27,7 @@ import javax.swing.*; import com.github.weisj.darklaf.icons.RotatableIcon; import com.github.weisj.darklaf.util.Alignment; +import com.github.weisj.darklaf.util.DarkUIUtil; public class RotatableIconAnimator extends Timer { @@ -43,8 +44,6 @@ public class RotatableIconAnimator extends Timer { super(100, null); if (icon == null) throw new IllegalArgumentException("Icon is null"); - if (parent == null) - throw new IllegalArgumentException("Component is null"); addActionListener(this::onAction); setRepeats(true); this.icon = icon; @@ -53,16 +52,26 @@ public class RotatableIconAnimator extends Timer { } public void resume() { - start(); + if (!isRunning()) + start(); } public void onAction(final ActionEvent e) { icon.setRotation(Math.PI * 2 * (((double) frame) / frameCount)); - parent.repaint(); + repaint(); frame = (frame + 1) % frameCount; } + protected void repaint() { + DarkUIUtil.repaint(getParent()); + } + + public JComponent getParent() { + return parent; + } + public void suspend() { - stop(); + if (isRunning()) + stop(); } } 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 index 0e43578b..5803c6f0 100644 --- 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 @@ -22,30 +22,38 @@ package com.github.weisj.darklaf.components.filetree; import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import javax.swing.*; import javax.swing.filechooser.FileSystemView; import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + public class FileTree extends JTree { public FileTree() { - this(null); + this((File[]) null); } - public FileTree(final File rootFile) { - this(rootFile, false); + public FileTree(final File... rootFile) { + this(false, rootFile); } - public FileTree(final File rootFile, final boolean showHiddenFiles) { + public FileTree(final boolean showHiddenFiles, final File... rootFiles) { FileSystemView fileSystemView = FileSystemView.getFileSystemView(); - setFileTreeModel(createModel(fileSystemView, rootFile, showHiddenFiles)); + setFileTreeModel(createModel(fileSystemView, showHiddenFiles, rootFiles)); setCellRenderer(new FileTreeCellRenderer(fileSystemView)); setRootVisible(false); } - protected FileTreeModel createModel(final FileSystemView fsv, final File rootFile, final boolean showHiddenFiles) { - return new FileTreeModel(fsv, rootFile, showHiddenFiles); + protected FileTreeModel createModel(final FileSystemView fsv, final boolean showHiddenFiles, + final File... rootFiles) { + return new FileTreeModel(fsv, showHiddenFiles, rootFiles); } public boolean isShowHiddenFiles() { @@ -71,4 +79,19 @@ public class FileTree extends JTree { public void reload() { getModel().reload(); } + + public Path getSelectedFile() { + TreePath path = getSelectionPath(); + if (path == null) + return null; + return ((FileTreeNode) path.getLastPathComponent()).getFile(); + } + + public List getSelectedFiles() { + TreePath[] paths = getSelectionPaths(); + if (paths == null) + return Collections.emptyList(); + return Arrays.stream(getSelectionPaths()).map(TreePath::getLastPathComponent).map(FileTreeNode.class::cast) + .map(FileTreeNode::getFile).collect(Collectors.toList()); + } } 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 index 94fee486..7f2b5267 100644 --- 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 @@ -39,7 +39,8 @@ public class FileTreeCellRenderer extends DefaultTreeCellRenderer { @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(); + FileTreeNode node = ((FileTreeNode) value); + Path f = node.getFile(); if (f != null) { setIcon(fsv.getSystemIcon(f.toFile())); setText(fsv.getSystemDisplayName(f.toFile())); 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 index 00faf914..619b1fab 100644 --- 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 @@ -23,6 +23,8 @@ package com.github.weisj.darklaf.components.filetree; import java.io.File; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; import javax.swing.filechooser.FileSystemView; import javax.swing.tree.DefaultTreeModel; @@ -33,28 +35,29 @@ public class FileTreeModel extends DefaultTreeModel { protected boolean showHiddenFiles; public FileTreeModel(final FileSystemView fileSystemView) { - this(fileSystemView, (Path) null, false); + this(fileSystemView, false, (Path[]) null); } - 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 boolean showHiddenFiles, final File... roots) { + this(fileSystemView, showHiddenFiles, + roots != null ? Arrays.stream(roots).map(File::toPath).toArray(Path[]::new) : null); } - public FileTreeModel(final FileSystemView fileSystemView, final Path root, final boolean showHiddenFiles) { + public FileTreeModel(final FileSystemView fileSystemView, final boolean showHiddenFiles, final Path... roots) { super(null); init(); this.showHiddenFiles = showHiddenFiles; this.fsv = fileSystemView; - this.root = createRoot(root); + this.root = createRoot(roots); } protected void init() {} - protected FileTreeNode createRoot(final Path root) { - if (root == null) { - return new FileTreeNode.RootNode(this); + protected FileTreeNode createRoot(final Path... roots) { + if (roots == null || roots.length != 1) { + return new FileTreeNode.RootNode(this, roots == null ? Collections.emptyList() : Arrays.asList(roots)); } else { - return createNode(null, root); + return createNode(null, roots[0]); } } 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 index 273ecaa8..0a8a02b7 100644 --- 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 @@ -24,9 +24,9 @@ package com.github.weisj.darklaf.components.filetree; import java.io.IOException; import java.nio.file.*; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Function; import java.util.stream.Stream; import javax.swing.*; @@ -37,6 +37,7 @@ public class FileTreeNode implements TreeNode, Comparable { protected final FileTreeNode parent; protected final FileTreeModel model; protected final Path file; + protected AtomicInteger taskCount = new AtomicInteger(); protected AtomicReference> children; protected WatchKey watchKey; @@ -75,18 +76,13 @@ public class FileTreeNode implements TreeNode, Comparable { 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) { + private int 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}); + if (index >= 0) { + nodes.remove(node); + model.unregister(node); + } + return index; } public void reload() { @@ -99,30 +95,45 @@ public class FileTreeNode implements TreeNode, Comparable { 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); - } + this.doInBackground(pub -> { + taskCount.getAndIncrement(); + this.traverseChildren(s -> { + Stream.concat(s.map(this::toNode), fileList.stream()).map(n -> { + if (Files.notExists(n.file)) { + return ReloadOp.remove(n, remove(fileList, n)); } else { - if (isHidden(node.file)) { - remove(fileList, node); - } else if (!fileList.contains(node)) { - add(fileList, node); + if (model.showHiddenFiles) { + if (!fileList.contains(n)) { + return ReloadOp.add(n, addSorted(fileList, n)); + } + } else { + if (isHidden(n.file)) { + return ReloadOp.remove(n, remove(fileList, n)); + } else if (!fileList.contains(n)) { + return ReloadOp.add(n, addSorted(fileList, n)); + } } } + return null; + }).forEach(pub); + }); + }, chunk -> { + for (ReloadOp op : chunk) { + if (op != null) { + switch (op.type) { + case ADD: + model.nodesWereInserted(FileTreeNode.this, new int[] {op.index}); + break; + case REMOVE: + model.nodesWereRemoved(FileTreeNode.this, new int[] {op.index}, new Object[] {op.node}); + break; + } } } }, () -> { if (depth > 0) fileList.forEach(n -> n.reload(depth - 1)); + taskCount.getAndDecrement(); }); } @@ -132,18 +143,14 @@ public class FileTreeNode implements TreeNode, Comparable { 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); - }); + this.doInBackground(pub -> { + traverseChildren(s -> { + s.filter(p -> model.showHiddenFiles || !isHidden(p)).map(this::toNode) + .map(n -> addSorted(fileList, n)).forEach(pub); + }); + }, chunks -> { + model.nodesWereInserted(FileTreeNode.this, chunks.stream().mapToInt(Integer::intValue).toArray()); + }); return fileList; }); } @@ -156,24 +163,17 @@ public class FileTreeNode implements TreeNode, Comparable { } } - 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 FileTreeNode toNode(final Path path) { + return model.createNode(FileTreeNode.this, path); } - 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 traverseChildren(final Consumer> consumer) { + if (Files.isDirectory(file)) { + try (Stream files = Files.walk(file, 1, FileVisitOption.FOLLOW_LINKS)) { + consumer.accept(files.filter(p -> !file.equals(p)).filter(Files::isReadable)); + } catch (IOException ignored) { } - }; + } } private void doInBackground(final Consumer> task, final Consumer> processor) { @@ -263,12 +263,19 @@ public class FileTreeNode implements TreeNode, Comparable { } } + public boolean isBusy() { + return taskCount.get() > 0; + } + public static class RootNode extends FileTreeNode { - public RootNode(final FileTreeModel model) { + private final List roots; + + public RootNode(final FileTreeModel model, final List roots) { super(null, null, model); + this.roots = roots; List nodes = new ArrayList<>(); - FileSystems.getDefault().getRootDirectories().forEach(p -> { + createInitialDirectories().forEach(p -> { FileTreeNode node = model.createNode(RootNode.this, p); model.register(node); nodes.add(node); @@ -276,12 +283,16 @@ public class FileTreeNode implements TreeNode, Comparable { children.set(nodes); } + protected Iterable createInitialDirectories() { + return roots.size() > 0 ? roots : FileSystems.getDefault().getRootDirectories(); + } + @Override protected void reload(final int depth) { if (depth < 0) return; List nodes = children.get(); - FileSystems.getDefault().getRootDirectories().forEach(p -> { + createInitialDirectories().forEach(p -> { FileTreeNode node = model.createNode(this, p); if (!nodes.contains(node)) { model.register(node); @@ -310,4 +321,30 @@ public class FileTreeNode implements TreeNode, Comparable { return true; } } + + private static class ReloadOp { + + private final Type type; + private final FileTreeNode node; + private final int index; + + private ReloadOp(final Type type, final FileTreeNode node, final int index) { + this.type = type; + this.node = node; + this.index = index; + } + + private static ReloadOp add(final FileTreeNode n, final int index) { + return new ReloadOp(Type.ADD, n, index); + } + + private static ReloadOp remove(final FileTreeNode n, final int index) { + return new ReloadOp(Type.REMOVE, n, index); + } + + private enum Type { + ADD, REMOVE + } + + } } 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 index 411199a1..5832b193 100644 --- 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 @@ -29,20 +29,21 @@ import javax.swing.filechooser.FileSystemView; public class WatchFileTree extends FileTree { public WatchFileTree() { - super(null); + super(); } - public WatchFileTree(final File rootFile) { - super(rootFile, false); + public WatchFileTree(final File... rootFiles) { + super(rootFiles); } - public WatchFileTree(final File rootFile, final boolean showHiddenFiles) { - super(rootFile, showHiddenFiles); + public WatchFileTree(final boolean showHiddenFiles, final File... rootFiles) { + super(showHiddenFiles, rootFiles); } @Override - protected FileTreeModel createModel(final FileSystemView fsv, final File rootFile, final boolean showHiddenFiles) { - return new WatchFileTreeModel(fsv, rootFile, showHiddenFiles); + protected FileTreeModel createModel(final FileSystemView fsv, final boolean showHiddenFiles, + final File... rootFiles) { + return new WatchFileTreeModel(fsv, showHiddenFiles, rootFiles); } @Override 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 index dfb677e4..bfe760f7 100644 --- 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 @@ -24,10 +24,7 @@ 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.*; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -76,12 +73,12 @@ public class WatchFileTreeModel extends FileTreeModel { super(fileSystemView); } - public WatchFileTreeModel(final FileSystemView fileSystemView, final File root, final boolean showHiddenFiles) { - super(fileSystemView, root, showHiddenFiles); + public WatchFileTreeModel(final FileSystemView fileSystemView, final boolean showHiddenFiles, final File... roots) { + super(fileSystemView, showHiddenFiles, roots); } - public WatchFileTreeModel(final FileSystemView fileSystemView, final Path root, final boolean showHiddenFiles) { - super(fileSystemView, root, showHiddenFiles); + public WatchFileTreeModel(final FileSystemView fileSystemView, final boolean showHiddenFiles, final Path... roots) { + super(fileSystemView, showHiddenFiles, roots); } @Override diff --git a/core/src/test/java/ui/tree/FileTreeDemo.java b/core/src/test/java/ui/tree/FileTreeDemo.java index 00f97a1b..6bd63b9c 100644 --- a/core/src/test/java/ui/tree/FileTreeDemo.java +++ b/core/src/test/java/ui/tree/FileTreeDemo.java @@ -42,7 +42,7 @@ public class FileTreeDemo implements ComponentDemo { @Override public JComponent createComponent() { - FileTree tree = new WatchFileTree(null, false); + FileTree tree = new WatchFileTree(false); DemoPanel panel = new DemoPanel(new OverlayScrollPane(tree), new BorderLayout(), 0); JPanel controlPanel = panel.addControls(); controlPanel.add(new JCheckBox("Show hidden files") {