From 445bc6b16b41487599ab805572ff31a02c5f8f4b Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Sun, 5 Nov 2017 19:07:50 +0300 Subject: [PATCH] Add TreeUtil.accept to support TreeVisitor --- .../com/intellij/util/ui/tree/TreeUtil.java | 76 ++++ .../util/ui/tree/TreeModelWithDelay.java | 62 +++ .../util/ui/tree/TreeModelWithInvoker.java | 23 + .../util/ui/tree/TreeModelWrapper.java | 56 +++ .../util/ui/tree/TreeUtilAcceptTest.java | 423 ++++++++++++++++++ 5 files changed, 640 insertions(+) create mode 100644 platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithDelay.java create mode 100644 platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithInvoker.java create mode 100644 platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWrapper.java create mode 100644 platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeUtilAcceptTest.java diff --git a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java index 5b4acaf3a973..26cac065f60b 100644 --- a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java +++ b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java @@ -46,6 +46,7 @@ import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.*; import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; import static com.intellij.openapi.wm.IdeFocusManager.getGlobalInstance; @@ -1137,4 +1138,79 @@ public final class TreeUtil { public static Comparator getDisplayOrderComparator(@NotNull final JTree tree) { return Comparator.comparingInt(tree::getRowForPath); } + + /** + * Processes nodes in the specified tree. + * + * @param tree a tree, which nodes should be processed + * @param visitor a visitor that controls processing of tree nodes + * @param consumer a path consumer called on done + */ + public static void accept(@NotNull JTree tree, @NotNull TreeVisitor visitor, @Nullable Consumer consumer) { + Function> acceptor = getTreeAcceptor(tree); + if (acceptor != null) { + Promise promise = acceptor.apply(visitor); + if (consumer != null) promise.processed(path -> consumer.accept(path)); + } + else { + TreeModel model = tree.getModel(); + TreePath path = model == null ? null : accept(model, visitor); + if (consumer != null) consumer.accept(path); + } + } + + private static TreePath accept(@NotNull TreeModel model, @NotNull TreeVisitor visitor) { + Object root = model.getRoot(); + if (root != null) { + TreePath path = new TreePath(root); + switch (visitor.visit(path)) { + case INTERRUPT: + return path; // path is found + case CONTINUE: + return accept(model, visitor, path); + default: + break; // skip children and/or siblings + } + } + return null; + } + + private static TreePath accept(@NotNull TreeModel model, @NotNull TreeVisitor visitor, @NotNull TreePath path) { + ArrayDeque> stack = new ArrayDeque<>(); + stack.push(children(model, path)); + while (path != null) { + ArrayDeque siblings = stack.peek(); + if (siblings == null) return null; // nothing to process + + TreePath next = siblings.poll(); + if (next == null) { + assert siblings == stack.poll(); + path = path.getParentPath(); + } + else { + switch (visitor.visit(next)) { + case INTERRUPT: + return next; // path is found + case CONTINUE: + stack.push(children(model, path = next)); + break; + case SKIP_SIBLINGS: + siblings.clear(); + break; + case SKIP_CHILDREN: + break; + } + } + } + assert stack.isEmpty(); + return null; + } + + private static ArrayDeque children(@NotNull TreeModel model, @NotNull TreePath path) { + Object object = path.getLastPathComponent(); + int count = model.getChildCount(object); + ArrayDeque deque = new ArrayDeque<>(count); + for (int i = 0; i < count; i++) deque.add(path.pathByAddingChild(model.getChild(object, i))); + return deque; + } } diff --git a/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithDelay.java b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithDelay.java new file mode 100644 index 000000000000..56db41a3dd66 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithDelay.java @@ -0,0 +1,62 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.ui.tree; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +class TreeModelWithDelay extends TreeModelWrapper { + private final long delay; + + TreeModelWithDelay(@NotNull TreeModel model, long delay) { + super(model); + this.delay = delay; + } + + void pause() { + if (delay > 0) { + try { + Thread.sleep(delay); + } + catch (InterruptedException ignored) { + } + } + } + + @Override + public Object getRoot() { + pause(); + return super.getRoot(); + } + + @Override + public Object getChild(Object parent, int index) { + pause(); + return super.getChild(parent, index); + } + + @Override + public int getChildCount(Object parent) { + pause(); + return super.getChildCount(parent); + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + pause(); + return super.getIndexOfChild(parent, child); + } + + @Override + public boolean isLeaf(Object child) { + pause(); + return super.isLeaf(child); + } + + @Override + public void valueForPathChanged(TreePath path, Object value) { + pause(); + super.valueForPathChanged(path, value); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithInvoker.java b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithInvoker.java new file mode 100644 index 000000000000..5d9e834930f1 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWithInvoker.java @@ -0,0 +1,23 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.ui.tree; + +import com.intellij.util.concurrency.Invoker; +import com.intellij.util.concurrency.InvokerSupplier; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.TreeModel; + +class TreeModelWithInvoker extends TreeModelWrapper implements InvokerSupplier { + private final Invoker invoker; + + TreeModelWithInvoker(@NotNull TreeModel model, @NotNull Invoker invoker) { + super(model); + this.invoker = invoker; + } + + @NotNull + @Override + public Invoker getInvoker() { + return invoker; + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWrapper.java b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWrapper.java new file mode 100644 index 000000000000..fd49cc2a2b34 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeModelWrapper.java @@ -0,0 +1,56 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.ui.tree; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +class TreeModelWrapper implements TreeModel { + private final TreeModel model; + + TreeModelWrapper(@NotNull TreeModel model) { + this.model = model; + } + + @Override + public Object getRoot() { + return model.getRoot(); + } + + @Override + public Object getChild(Object parent, int index) { + return model.getChild(parent, index); + } + + @Override + public int getChildCount(Object parent) { + return model.getChildCount(parent); + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + return model.getIndexOfChild(parent, child); + } + + @Override + public boolean isLeaf(Object child) { + return model.isLeaf(child); + } + + @Override + public void valueForPathChanged(TreePath path, Object value) { + model.valueForPathChanged(path, value); + } + + @Override + public void addTreeModelListener(TreeModelListener listener) { + model.addTreeModelListener(listener); + } + + @Override + public void removeTreeModelListener(TreeModelListener listener) { + model.removeTreeModelListener(listener); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeUtilAcceptTest.java b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeUtilAcceptTest.java new file mode 100644 index 000000000000..d05e99ddf47b --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/util/ui/tree/TreeUtilAcceptTest.java @@ -0,0 +1,423 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.ui.tree; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.util.Disposer; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.ui.tree.AsyncTreeModel; +import com.intellij.ui.tree.TreeVisitor; +import com.intellij.util.ArrayUtil; +import com.intellij.util.concurrency.Invoker; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.AsyncPromise; +import org.junit.Assert; +import org.junit.Test; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import java.awt.EventQueue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.concurrent.TimeUnit.MINUTES; + +/** + * @author Sergey.Malenkov + */ +public final class TreeUtilAcceptTest { + @Test + public void testAcceptDepth1() { + testFind(() -> new DepthVisitor(1), 1); + } + + @Test + public void testAcceptDepth2() { + testFind(() -> new DepthVisitor(2), 4); + } + + @Test + public void testAcceptDepth3() { + testFind(() -> new DepthVisitor(3), 21); + } + + @Test + public void testAcceptAll() { + testFind(() -> new Visitor(), 21); + } + + + @Test + public void testFindRoot() { + testFind(() -> new StringFinder("Root"), 1, "Root"); + } + + @Test + public void testFindWrongRoot() { + testFind(() -> new StringFinder("ROOT"), 1); + } + + @Test + public void testFindColor() { + testFind(() -> new StringFinder("RootColor"), 2, "RootColor"); + } + + @Test + public void testFindWrongColor() { + testFind(() -> new StringFinder("RootCOLOR"), 4); + } + + @Test + public void testFindDigit() { + testFind(() -> new StringFinder("RootDigit"), 3, "RootDigit"); + } + + @Test + public void testFindWrongDigit() { + testFind(() -> new StringFinder("RootDIGIT"), 4); + } + + @Test + public void testFindGreek() { + testFind(() -> new StringFinder("RootGreek"), 4, "RootGreek"); + } + + @Test + public void testFindWrongGreek() { + testFind(() -> new StringFinder("RootGREEK"), 4); + } + + @Test + public void testFindGreen() { + testFind(() -> new StringFinder("RootColorGreen"), 4, "RootColorGreen"); + } + + @Test + public void testFindWrongGreen() { + testFind(() -> new StringFinder("RootColorGREEN"), 7); + } + + @Test + public void testFindFive() { + testFind(() -> new StringFinder("RootDigitFive"), 8, "RootDigitFive"); + } + + @Test + public void testFindWrongFive() { + testFind(() -> new StringFinder("RootDigitFIVE"), 13); + } + + @Test + public void testFindGamma() { + testFind(() -> new StringFinder("RootGreekGamma"), 7, "RootGreekGamma"); + } + + @Test + public void testFindWrongGamma() { + testFind(() -> new StringFinder("RootGreekGAMMA"), 9); + } + + private static void testFind(@NotNull Supplier supplier, long count) { + testFind(supplier, count, null); + } + + private static void testFind(@NotNull Supplier supplier, long count, String value) { + test((tree, promise) -> { + @NotNull Visitor visitor = supplier.get(); + TreeUtil.accept(tree, visitor, path -> invokeSafely(promise, () -> { + Assert.assertEquals(count, visitor.counter.get()); + Assert.assertEquals(value, value(path)); + })); + }); + } + + + @Test + public void testShowRoot() { + testShow(() -> new StringFinder("Root"), "+[Root]\n"); + } + + @Test + public void testShowColor() { + testShow(() -> new StringFinder("RootColor"), "-Root\n" + + " +[RootColor]\n" + + " +RootDigit\n" + + " +RootGreek\n"); + } + + @Test + public void testShowDigit() { + testShow(() -> new StringFinder("RootDigit"), "-Root\n" + + " +RootColor\n" + + " +[RootDigit]\n" + + " +RootGreek\n"); + } + + @Test + public void testShowGreek() { + testShow(() -> new StringFinder("RootGreek"), "-Root\n" + + " +RootColor\n" + + " +RootDigit\n" + + " +[RootGreek]\n"); + } + + @Test + public void testShowGreen() { + testShow(() -> new StringFinder("RootColorGreen"), "-Root\n" + + " -RootColor\n" + + " RootColorRed\n" + + " [RootColorGreen]\n" + + " RootColorBlue\n" + + " +RootDigit\n" + + " +RootGreek\n"); + } + + @Test + public void testShowFive() { + testShow(() -> new StringFinder("RootDigitFive"), "-Root\n" + + " +RootColor\n" + + " -RootDigit\n" + + " RootDigitOne\n" + + " RootDigitTwo\n" + + " RootDigitThree\n" + + " RootDigitFour\n" + + " [RootDigitFive]\n" + + " RootDigitSix\n" + + " RootDigitSeven\n" + + " RootDigitEight\n" + + " RootDigitNine\n" + + " +RootGreek\n"); + } + + @Test + public void testShowGamma() { + testShow(() -> new StringFinder("RootGreekGamma"), "-Root\n" + + " +RootColor\n" + + " +RootDigit\n" + + " -RootGreek\n" + + " RootGreekAlpha\n" + + " RootGreekBeta\n" + + " [RootGreekGamma]\n" + + " RootGreekDelta\n" + + " RootGreekEpsilon\n"); + } + + private static void testShow(@NotNull Supplier supplier, @NotNull String expected) { + test((tree, promise) -> { + @NotNull Visitor visitor = supplier.get(); + TreeUtil.accept(tree, visitor, path -> invokeSafely(promise, () -> { + Assert.assertNotNull(path); + tree.makeVisible(path); + tree.setSelectionPath(path); + PlatformTestUtil.assertTreeEqual(tree, expected, true); + })); + }); + } + + + @Test + public void testExpandOne() { + test((tree, promise) -> TreeUtil.expand(tree, 1, () -> invokeSafely(promise, () -> PlatformTestUtil.assertTreeEqual( + tree, "-Root\n" + + " +RootColor\n" + + " +RootDigit\n" + + " +RootGreek\n")))); + } + + @Test + public void testExpandAll() { + test((tree, promise) -> TreeUtil.expandAll(tree, () -> invokeSafely(promise, () -> PlatformTestUtil.assertTreeEqual( + tree, "-Root\n" + + " -RootColor\n" + + " RootColorRed\n" + + " RootColorGreen\n" + + " RootColorBlue\n" + + " -RootDigit\n" + + " RootDigitOne\n" + + " RootDigitTwo\n" + + " RootDigitThree\n" + + " RootDigitFour\n" + + " RootDigitFive\n" + + " RootDigitSix\n" + + " RootDigitSeven\n" + + " RootDigitEight\n" + + " RootDigitNine\n" + + " -RootGreek\n" + + " RootGreekAlpha\n" + + " RootGreekBeta\n" + + " RootGreekGamma\n" + + " RootGreekDelta\n" + + " RootGreekEpsilon\n")))); + } + + + private static void invokeLater(@NotNull AsyncPromise promise, @NotNull Runnable runnable) { + EventQueue.invokeLater(() -> invokeSafely(promise, runnable, false)); + } + + private static void invokeSafely(@NotNull AsyncPromise promise, @NotNull Runnable runnable) { + invokeSafely(promise, runnable, true); + } + + private static void invokeSafely(@NotNull AsyncPromise promise, @NotNull Runnable runnable, boolean stop) { + try { + runnable.run(); + if (stop) promise.setResult(null); + } + catch (Throwable throwable) { + promise.setResult(throwable); + } + } + + private static void test(BiConsumer> consumer) { + test(consumer, parent -> model(0, false, null)); + test(consumer, parent -> model(10, false, null)); + test(consumer, parent -> model(0, true, new Invoker.EDT(parent))); + test(consumer, parent -> model(0, false, new Invoker.EDT(parent))); + test(consumer, parent -> model(10, true, new Invoker.EDT(parent))); + test(consumer, parent -> model(10, false, new Invoker.EDT(parent))); + test(consumer, parent -> model(0, true, new Invoker.BackgroundThread(parent))); + test(consumer, parent -> model(0, false, new Invoker.BackgroundThread(parent))); + test(consumer, parent -> model(10, true, new Invoker.BackgroundThread(parent))); + test(consumer, parent -> model(10, false, new Invoker.BackgroundThread(parent))); + } + + private static void test(BiConsumer> consumer, Function function) { + if (EventQueue.isDispatchThread()) throw new IllegalThreadStateException("main thread is expected"); + AsyncPromise promise = new AsyncPromise<>(); + Disposable parent = Disposer.newDisposable(); + TreeModel model = function.apply(parent); + AsyncTreeModel atm = model instanceof AsyncTreeModel ? (AsyncTreeModel)model : null; + invokeLater(promise, () -> { + @SuppressWarnings("UndesirableClassUsage") + JTree tree = new JTree(model); + if (atm != null) TreeUtil.setTreeAcceptor(tree, atm::accept); + new Runnable() { + @Override + public void run() { + if (atm != null && atm.isProcessing()) { + invokeLater(promise, this); + } + else { + tree.collapseRow(0); // because root node is expanded by default + PlatformTestUtil.assertTreeEqual(tree, "+Root\n"); + consumer.accept(tree, promise); + } + } + }.run(); + }); + try { + Throwable throwable = promise.blockingGet(2, MINUTES); + if (throwable != null) throw new IllegalStateException("test failed", throwable); + } + finally { + Disposer.dispose(parent); + } + } + + private static TreeModel model(long delay, boolean showLoadingNode, Invoker invoker) { + TreeModel model = new DefaultTreeModel(root()); + if (delay > 0) { + model = new TreeModelWithDelay(model, delay); + } + if (invoker != null) { + model = new TreeModelWithInvoker(model, invoker); + model = new AsyncTreeModel(model, showLoadingNode); + Disposer.register(invoker, (Disposable)model); + } + return model; + } + + private static String value(TreePath path) { + return path == null ? null : path.getLastPathComponent().toString(); + } + + private static DefaultMutableTreeNode node(@NotNull Object object, Object... children) { + if (object instanceof DefaultMutableTreeNode && ArrayUtil.isEmpty(children)) return (DefaultMutableTreeNode)object; + if (object instanceof TreeNode) throw new IllegalArgumentException("do not use a tree node as a node content"); + DefaultMutableTreeNode node = new DefaultMutableTreeNode(object); + for (Object child : children) node.add(node(child)); + return node; + } + + private static DefaultMutableTreeNode root() { + return node("Root", + node("RootColor", + node("RootColorRed"), + node("RootColorGreen"), + node("RootColorBlue")), + node("RootDigit", + node("RootDigitOne"), + node("RootDigitTwo"), + node("RootDigitThree"), + node("RootDigitFour"), + node("RootDigitFive"), + node("RootDigitSix"), + node("RootDigitSeven"), + node("RootDigitEight"), + node("RootDigitNine")), + node("RootGreek", + node("RootGreekAlpha"), + node("RootGreekBeta"), + node("RootGreekGamma"), + node("RootGreekDelta"), + node("RootGreekEpsilon"))); + } + + static class Visitor extends TreeVisitor.Finder { + final AtomicLong counter = new AtomicLong(); + + @NotNull + @Override + public Action visit(@NotNull TreePath path) { + counter.incrementAndGet(); + return super.visit(path); + } + + @Override + protected boolean found(@NotNull TreePath path) { + return false; + } + + @Override + protected boolean contains(@NotNull TreePath path) { + return true; + } + } + + static class DepthVisitor extends Visitor { + private final int depth; + + DepthVisitor(int depth) { + this.depth = depth; + } + + @Override + protected boolean contains(@NotNull TreePath path) { + return depth > path.getPathCount(); + } + } + + static class StringFinder extends Visitor { + private final String value; + + StringFinder(@NotNull String value) { + this.value = value; + } + + @Override + protected boolean found(@NotNull TreePath path) { + return value.equals(value(path)); + } + + @Override + protected boolean contains(@NotNull TreePath path) { + return value.startsWith(value(path)); + } + } +}