/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.ide.util.treeView; import com.intellij.ide.projectView.PresentationData; import com.intellij.openapi.util.AsyncResult; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.ui.SimpleTextAttributes; import com.intellij.ui.treeStructure.Tree; import com.intellij.util.WaitFor; import com.intellij.util.containers.HashMap; import junit.framework.Assert; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.util.*; import static com.intellij.testFramework.PlatformTestUtil.notNull; abstract class AbstractTreeBuilderTest extends BaseTreeTestCase<BaseTreeTestCase.NodeElement> { protected MyStructure myStructure; Node myRoot; DefaultTreeModel myTreeModel; Node myCom; Node myOpenApi; Node myIde; Node myRunner; Node myRcp; boolean myEnsureSelection = false; AbstractTreeBuilderTest.Node myFabrique; Map<NodeElement, ElementEntry> myElementUpdate = new TreeMap<>(); ElementUpdateHook myElementUpdateHook; Map<String, Integer> mySortedParent = new TreeMap<>(); NodeDescriptor.NodeComparator.Delegate<NodeDescriptor> myComparator; Node myIntellij; protected final Set<NodeElement> myChanges = new HashSet<>(); protected AbstractTreeBuilderTest(boolean passThrough) { super(passThrough); } protected AbstractTreeBuilderTest(boolean yieldingUiBuild, boolean bgStructureBuilding) { super(yieldingUiBuild, bgStructureBuilding); } @Override protected void setUp() throws Exception { super.setUp(); myComparator = new NodeDescriptor.NodeComparator.Delegate<>(new NodeDescriptor.NodeComparator<NodeDescriptor>() { @Override public int compare(NodeDescriptor o1, NodeDescriptor o2) { return AlphaComparator.INSTANCE.compare(o1, o2); } }); mySortedParent.clear(); myTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode(null)); myTreeModel.addTreeModelListener(new TreeModelListener() { @Override public void treeNodesChanged(TreeModelEvent e) { assertEdt(); } @Override public void treeNodesInserted(TreeModelEvent e) { assertEdt(); } @Override public void treeNodesRemoved(TreeModelEvent e) { assertEdt(); } @Override public void treeStructureChanged(TreeModelEvent e) { assertEdt(); } }); myTree = new Tree(myTreeModel); myStructure = new MyStructure(); myRoot = new Node(null, "/"); initBuilder(new MyBuilder()); myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { assertEdt(); } }); myTree.addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent event) { assertEdt(); } @Override public void treeCollapsed(TreeExpansionEvent event) { assertEdt(); } }); } @Override protected void tearDown() throws Exception { myElementUpdate.clear(); myElementUpdateHook = null; myStructure.setReValidator(null); super.tearDown(); } @Nullable Node removeFromParentButKeepRef(NodeElement child) { NodeElement parent = (NodeElement)myStructure.getParentElement(child); AbstractTreeBuilderTest.Node node = myStructure.getNodeFor(parent).remove(child, false); Assert.assertEquals(parent, myStructure.getParentElement(child)); Assert.assertFalse(Arrays.asList(myStructure._getChildElements(parent, false)).contains(child)); return node; } void assertSorted(String expected) { Iterator<String> keys = mySortedParent.keySet().iterator(); StringBuilder result = new StringBuilder(); while (keys.hasNext()) { String each = keys.next(); result.append(each); int count = mySortedParent.get(each); if (count > 1) { result.append(" (").append(count).append(")"); } if (keys.hasNext()) { result.append("\n"); } } Assert.assertEquals(expected, result.toString()); mySortedParent.clear(); } private void addEntry(String value) { if (mySortedParent.containsKey(value)) { mySortedParent.put(value, mySortedParent.get(value) + 1); } else { mySortedParent.put(value, 0); } } void assertUpdates(String expected) { List<Object> entries = Arrays.asList(myElementUpdate.values().toArray()); Assert.assertEquals(expected + "\n", PlatformTestUtil.print(entries) + "\n"); myElementUpdate.clear(); } void buildStructure(final Node root) throws Exception { buildStructure(root, true); } void buildStructure(final Node root, final boolean activate) throws Exception { doAndWaitForBuilder(() -> { myCom = root.addChild("com"); myIntellij = myCom.addChild("intellij"); myOpenApi = myIntellij.addChild("openapi"); myFabrique = root.addChild("jetbrains").addChild("fabrique"); myIde = myFabrique.addChild("ide"); myRunner = root.addChild("xUnit").addChild("runner"); myRcp = root.addChild("org").addChild("eclipse").addChild("rcp"); if (activate) { getBuilder().getUi().activate(true); } }); } protected void activate() throws Exception { doAndWaitForBuilder(() -> getBuilder().getUi().activate(true)); } void hideTree() throws Exception { Assert.assertFalse(getMyBuilder().myWasCleanedUp); invokeLaterIfNeeded(() -> getBuilder().getUi().deactivate()); final WaitFor waitFor = new WaitFor() { @Override protected boolean condition() { return getMyBuilder().myWasCleanedUp || myCancelRequest != null; } }; if (myCancelRequest != null) { throw new Exception(myCancelRequest); } waitFor.assertCompleted("Tree cleanup was not performed. isCancelledReadyState=" + getBuilder().getUi().isCancelledReady()); Assert.assertTrue(getMyBuilder().myWasCleanedUp); } void buildNode(String elementText, boolean select) throws Exception { buildNode(new NodeElement(elementText), select); } void buildNode(Node node, boolean select) throws Exception { buildNode(node.myElement, select); } void buildNode(final NodeElement element, final boolean select) throws Exception { buildNode(element, select, true); } void buildNode(final NodeElement element, final boolean select, final boolean addToSelection) throws Exception { final boolean[] done = new boolean[] {false}; doAndWaitForBuilder(() -> { if (select) { getBuilder().select(element, () -> done[0] = true, addToSelection); } else { getBuilder().expand(element, () -> done[0] = true); } }, o -> done[0]); Assert.assertNotNull(findNode(element, select)); } @Nullable DefaultMutableTreeNode findNode(String elementText, boolean shouldBeSelected) { return findNode(new NodeElement(elementText), shouldBeSelected); } @Nullable DefaultMutableTreeNode findNode(NodeElement element, boolean shouldBeSelected) { return findNode((DefaultMutableTreeNode)myTree.getModel().getRoot(), element, shouldBeSelected); } @Nullable private DefaultMutableTreeNode findNode(DefaultMutableTreeNode treeNode, NodeElement toFind, boolean shouldBeSelected) { final Object object = treeNode.getUserObject(); Assert.assertNotNull(object); if (!(object instanceof NodeDescriptor)) return null; final NodeElement element = (NodeElement)((NodeDescriptor)object).getElement(); if (toFind.equals(element)) return treeNode; for (int i = 0; i < treeNode.getChildCount(); i++) { final DefaultMutableTreeNode result = findNode((DefaultMutableTreeNode)treeNode.getChildAt(i), toFind, shouldBeSelected); if (result != null) { if (shouldBeSelected) { final TreePath path = new TreePath(result.getPath()); Assert.assertTrue("Path should be selected: " + path, myTree.isPathSelected(path)); } return result; } } return null; } class Node { final NodeElement myElement; final ArrayList<Node> myChildElements = new ArrayList<>(); Node(Node parent, String textName) { this(parent, new NodeElement(textName)); } Node(Node parent, NodeElement name) { myElement = name; setParent(parent); } private void setParent(Node parent) { myStructure.register(parent != null ? parent.myElement : null, this); } public NodeElement getElement() { return myElement; } public Node addChild(Node node) { myChildElements.add(node); node.setParent(this); return node; } public Node addChild(String name) { final Node node = new Node(this, name); myChildElements.add(node); return node; } public Node addChild(NodeElement element) { final Node node = new Node(this, element); myChildElements.add(node); return node; } @Override public String toString() { return myElement.toString(); } public void removeAll() { myChildElements.clear(); } @Nullable public Node getChildNode(String name) { for (Node each : myChildElements) { if (name.equals(each.myElement.myName)) return each; } return null; } public Object[] getChildElements() { Object[] elements = new Object[myChildElements.size()]; for (int i = 0; i < myChildElements.size(); i++) { elements[i] = myChildElements.get(i).myElement; } return elements; } public void delete() { final NodeElement parent = (NodeElement)myStructure.getParentElement(myElement); Assert.assertNotNull(myElement.toString(), parent); myStructure.getNodeFor(parent).remove(myElement, true); } @Nullable private Node remove(final NodeElement name, boolean removeRefToParent) { final Iterator<Node> kids = myChildElements.iterator(); Node removed = null; while (kids.hasNext()) { Node each = kids.next(); if (name.equals(each.myElement)) { kids.remove(); removed = each; break; } } if (removeRefToParent) { myStructure.myChild2Parent.remove(name); } return removed; } } class MyStructure extends BaseStructure { private final Map<NodeElement, NodeElement> myChild2Parent = new HashMap<>(); private final Map<NodeElement, Node> myElement2Node = new HashMap<>(); private final Set<NodeElement> myLeaves = new HashSet<>(); private ReValidator myReValidator; @Override public Object getRootElement() { return myRoot.myElement; } public void reInitRoot(Node root) { myRoot = root; myElement2Node.clear(); myLeaves.clear(); myElement2Node.put(root.myElement, root); } @Override public Object[] doGetChildElements(Object element) { onElementAction("getChildren", (NodeElement)element); final AbstractTreeBuilderTest.Node node = myElement2Node.get((NodeElement)element); return node.getChildElements(); } @Override public Object getParentElement(final Object element) { NodeElement nodeElement = (NodeElement)element; return nodeElement.getForcedParent() != null ? nodeElement.getForcedParent() : myChild2Parent.get(nodeElement); } @Override public boolean isAlwaysLeaf(Object element) { //noinspection SuspiciousMethodCalls return myLeaves.contains(element); } public void addLeaf(NodeElement element) { myLeaves.add(element); } public void removeLeaf(NodeElement element) { myLeaves.remove(element); } @Override @NotNull public NodeDescriptor doCreateDescriptor(final Object element, final NodeDescriptor parentDescriptor) { return new PresentableNodeDescriptor(null, parentDescriptor) { @Override protected void update(PresentationData presentation) { onElementAction("update", (NodeElement)element); presentation.clear(); presentation.addText(new ColoredFragment(getElement().toString(), SimpleTextAttributes.REGULAR_ATTRIBUTES)); if (myChanges.contains(element)) { myChanges.remove(element); presentation.setChanged(true); } } @Nullable @Override public PresentableNodeDescriptor getChildToHighlightAt(int index) { return null; } @Override public Object getElement() { return element; } @Override public String toString() { List<ColoredFragment> coloredText = getPresentation().getColoredText(); StringBuilder result = new StringBuilder(); for (ColoredFragment each : coloredText) { result.append(each.getText()); } return result.toString(); } }; } public void register(final NodeElement parent, final Node child) { myChild2Parent.put(child.myElement, parent); myElement2Node.put(child.myElement, child); } public void clear() { myChild2Parent.clear(); myElement2Node.clear(); myElement2Node.put(myRoot.myElement, myRoot); } public Node getNodeFor(NodeElement element) { return myElement2Node.get(element); } @Override public AsyncResult<Object> revalidateElement(Object element) { return myReValidator != null ? myReValidator.revalidate((NodeElement)element) : super.revalidateElement(element); } public void setReValidator(@Nullable ReValidator reValidator) { myReValidator = reValidator; } } interface ReValidator { AsyncResult<Object> revalidate(NodeElement element); } class MyBuilder extends BaseTreeBuilder { public MyBuilder() { super(AbstractTreeBuilderTest.this.myTree, AbstractTreeBuilderTest.this.myTreeModel, AbstractTreeBuilderTest.this.myStructure, myComparator, false); initRootNode(); } @Override protected void sortChildren(Comparator<TreeNode> nodeComparator, DefaultMutableTreeNode node, ArrayList<TreeNode> children) { super.sortChildren(nodeComparator, node, children); addEntry(node.toString()); } @Override public boolean isToEnsureSelectionOnFocusGained() { return myEnsureSelection; } } private void onElementAction(String action, NodeElement element) { myElementUpdate.computeIfAbsent(element, k -> new ElementEntry(element)).onElementAction(action); if (myElementUpdateHook != null) { myElementUpdateHook.onElementAction(action, element); } } interface ElementUpdateHook { void onElementAction(String action, Object element); } private class ElementEntry { NodeElement myElement; int myUpdateCount; int myGetChildrenCount; private ElementEntry(NodeElement element) { myElement = element; } void onElementAction(String action) { try { if ("update".equals(action)) { myUpdateCount++; } else if ("getChildren".equals(action)) { Assert.assertTrue("getChildren() is called before update(), node=" + myElement, myUpdateCount > 0); myGetChildrenCount++; } } catch (Throwable e) { myCancelRequest = e; } } @Override public String toString() { return (myElement + ": " + asString(myUpdateCount, "update") + " " + asString(myGetChildrenCount, "getChildren")).trim(); } private String asString(int count, String text) { if (count == 0) return ""; return count > 1 ? text + " (" + count + ")" : text; } } MyBuilder getMyBuilder() { return (MyBuilder)getBuilder(); } interface TreeAction { void run(Runnable onDone); } TreePath getPath(String s) { return new TreePath(notNull(findNode(s, false)).getPath()); } }