diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeLazyNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeLazyNode.java index b9f4de5fbe..92e365f386 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeLazyNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeLazyNode.java @@ -22,7 +22,9 @@ import java.util.List; * Also, children of this node can be unloaded by calling {@link #unloadChildren()}. This * can be used by nodes in large trees to save memory by unloading children that are no longer * in the current tree view (collapsed). Of course, that decision would need to be balanced - * against the extra time to reload the nodes in the event that a filter is applied. + * against the extra time to reload the nodes in the event that a filter is applied. Also, if + * some external event occurs that changes the set of children for a GTreeLazyNode, you can call + * {@link #reload()} to refresh the node's children. */ public abstract class GTreeLazyNode extends GTreeNode { @@ -36,6 +38,9 @@ public abstract class GTreeLazyNode extends GTreeNode { /** * Sets this lazy node back to the "unloaded" state such that if * its children are accessed, it will reload its children as needed. + * NOTE: This method does not trigger a call to {@link #fireNodeChanged(GTreeNode, GTreeNode)} + * because doing will often trigger a call from the JTree will will immediately cause the node + * to reload its children. If that is the effect you want, call {@link #reload()}. */ public void unloadChildren() { if (isLoaded()) { @@ -43,6 +48,19 @@ public abstract class GTreeLazyNode extends GTreeNode { } } + /** + * Tells this node that its children are stale and that it needs to regenerate them. This will + * unload any existing children and call {@link #fireNodeStructureChanged(GTreeNode)} which will + * inform the JTree that this node has changed and when the JTree queries this node for its children, + * the {@link #generateChildren()} will get called to populate the node. + */ + public void reload() { + if (isLoaded()) { + unloadChildren(); + fireNodeStructureChanged(this); + } + } + @Override public void addNode(GTreeNode node) { if (isLoaded()) { @@ -66,9 +84,7 @@ public abstract class GTreeLazyNode extends GTreeNode { @Override public void removeAll() { - if (isLoaded()) { - unloadChildren(); - } + reload(); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java index fd94b35057..d41c7e3f17 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java @@ -441,6 +441,42 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable doFireNodeChanged()); } + /** + * Convenience method for expanding (opening) this node in the tree. If this node is not + * currently attached to a visible tree, then this call does nothing + */ + public void expand() { + GTree tree = getTree(); + if (tree != null) { + tree.expandPath(this); + } + } + + /** + * Convenience method for collapsing (closing) this node in the tree. If this node is not + * currently attached to a visible tree, then this call does nothing + */ + public void collapse() { + GTree tree = getTree(); + if (tree != null) { + tree.collapseAll(this); + } + } + + /** + * Convenience method determining if this node is expanded in a tree. If the node is not + * currently attached to a visible tree, then this call returns false + * + * @return true if the node is expanded in a currently visible tree. + */ + public boolean isExpanded() { + GTree tree = getTree(); + if (tree != null) { + return tree.isExpanded(this.getTreePath()); + } + return false; + } + private GTreeNode[] getPathToRoot(GTreeNode node, int depth) { GTreeNode[] returnNodes; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeRestoreTreeStateTask.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeRestoreTreeStateTask.java index 33df759d2e..294c362f95 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeRestoreTreeStateTask.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeRestoreTreeStateTask.java @@ -56,7 +56,7 @@ public class GTreeRestoreTreeStateTask extends GTreeTask { monitor.setMessage("Restoring tree selection state"); selectPathsInThisTask(state, monitor, true); - // this allows some tress to perform cleanup + // this allows some trees to perform cleanup tree.expandedStateRestored(monitor); tree.clearFilterRestoreState(); } diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java index 85e894ee6d..81f2dbcf79 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/tree/GTreeTest.java @@ -187,6 +187,25 @@ public class GTreeTest extends AbstractDockingTest { assertNull("Found a node in the tree that should have been filtered out", node2); } + @Test + public void testExpandCollapseNode() { + GTreeNode node = findNodeInTree(NonLeafWithOneLevelOfChildrenNodeB.class.getSimpleName()); + assertTrue(!node.isExpanded()); + + node.expand(); + waitForTree(); + assertTrue(node.isExpanded()); + + node.collapse(); + waitForTree(); + assertTrue(!node.isExpanded()); + + GTreeNode root = node.getRoot(); + root.collapse(); + assertTrue(!root.isExpanded()); + assertTrue(gTree.getExpandedPaths().isEmpty()); + } + @Test public void testChangeFilterSettingsWithFilterTextInPlace() { diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/tree/GTreeNodeTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/tree/GTreeNodeTest.java index 01f3b261aa..0d09fed43d 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/tree/GTreeNodeTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/tree/GTreeNodeTest.java @@ -25,10 +25,23 @@ import javax.swing.tree.TreePath; import org.junit.Before; import org.junit.Test; +import docking.test.AbstractDockingTest; import docking.widgets.tree.support.GTreeFilter; +import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; +/** + * Note: This test does not extend {@link AbstractDockingTest}. Extending that class sets up + * {@link Swing#runNow(Runnable)} methods so that they actually run on the swing thread; otherwise + * they run on the calling thread. Normally GTreeNode test would need that because the fire event + * calls normally check that the events are being sent on the swing thread. In this file, all the + * tests use {@link TestNode} or {@link LazyTestNode} which override the event sending methods to + * instead put the events in a list so that the test can check that the correct events were generated. + * Since the methods that check for being on the swing thread are overridden, we can get away with + * not extending {@link AbstractDockingTest} and this allows the test to run about 100 times faster. + */ + public class GTreeNodeTest { private List events = new ArrayList<>(); private GTreeNode root; @@ -385,11 +398,35 @@ public class GTreeNodeTest { } @Test - public void testLoadAllOnLazyTre() throws CancelledException { - GTreeNode node = new LazyGTestNode("test", 2); + public void testLoadAllOnLazyTree() throws CancelledException { + GTreeNode node = new LazyTestNode("test", 2); assertEquals(13, node.loadAll(TaskMonitor.DUMMY)); } + @Test + public void testUnloadOnLazyNode() throws CancelledException { + GTreeLazyNode node = new LazyTestNode("test", 2); + node.loadAll(TaskMonitor.DUMMY); + assertTrue(node.isLoaded()); + + events.clear(); + node.unloadChildren(); + assertTrue(!node.isLoaded()); + assertTrue(events.isEmpty()); + } + + @Test + public void testReloadOnLazyNode() throws CancelledException { + GTreeLazyNode node = new LazyTestNode("test", 2); + node.loadAll(TaskMonitor.DUMMY); + assertTrue(node.isLoaded()); + + events.clear(); + node.reload(); + assertTrue(!node.isLoaded()); + assertTrue(!events.isEmpty()); + } + @Test public void testStreamDepthFirst() { List collect = root.stream(true).collect(Collectors.toList()); @@ -454,6 +491,33 @@ public class GTreeNodeTest { } + private class LazyTestNode extends LazyGTestNode { + + LazyTestNode(String name, int depth) { + super(name, depth); + } + + @Override + public void doFireNodeStructureChanged() { + events.add(new TestEvent(EventType.STRUCTURE_CHANGED, null, this, -1)); + } + + @Override + public void doFireNodeChanged() { + events.add(new TestEvent(EventType.NODE_CHANGED, getParent(), this, -1)); + } + + @Override + protected void doFireNodeAdded(GTreeNode newNode) { + events.add(new TestEvent(EventType.NODE_ADDED, this, newNode, -1)); + } + + @Override + protected void doFireNodeRemoved(GTreeNode removedNode, int index) { + events.add(new TestEvent(EventType.NODE_REMOVED, this, removedNode, -1)); + } + } + private class TestNode extends GTestNode { TestNode(String name) { super(name); @@ -480,6 +544,7 @@ public class GTreeNodeTest { } } + enum EventType { STRUCTURE_CHANGED, NODE_CHANGED, NODE_ADDED, NODE_REMOVED }