Added some convenience methods to GTree nodes.

This commit is contained in:
ghidravore 2020-01-10 16:43:59 -05:00
parent 3ce8d3fa39
commit 7cfa80f8ac
5 changed files with 143 additions and 7 deletions

View file

@ -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

View file

@ -441,6 +441,42 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
Swing.runNow(() -> 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;

View file

@ -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();
}

View file

@ -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() {

View file

@ -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<TestEvent> 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<GTreeNode> 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
}