GP-1193 Improved behavior of symbol tree when bulk operations add/delete/rename large numbers of symbols

This commit is contained in:
ghidravore 2021-08-20 14:30:36 -04:00
parent 5e5d474279
commit bf27b8e410
8 changed files with 149 additions and 17 deletions

View file

@ -78,12 +78,18 @@
</LI>
</UL>
<P align="left"><A name="GroupNode"></A>When a category or symbol has many elements, the
<P align="left"><A name="GroupNode"></A>When the label or function category has many elements, the
Symbol Tree will show <I>group nodes</I> that represent groups of symbols in order to reduce
the "clutter" in the tree. In the sample image above, the <I>Labels</I> category contains a
group node (indicated by the <IMG alt="" src="images/openFolderGroup.png"> icon). The
<I>group node</I> shows the common prefix for all the symbols in the group. The tool tip
will display the total number of nodes in the group.</P>
<P>The group nodes can become out of balance if enough symbols are added/deleted/renamed. The
entire category can be reorganized by closing and reopening the category node (Functions, Labels).
Also, the tree will detect if the organization becomes too far out of balance. In this case, the
category node (Functions, Labels) will automatically close. Typically, this would be caused by
a long running bulk operation such as analysis. This has the added benefit of
speeding up the bulk operation as it will no longer have to keep updating the nodes in the tree.</P>
<BLOCKQUOTE>
<P align="left"><IMG alt="" src="../../shared/note.png"> The Symbol Tree does not show

View file

@ -23,6 +23,8 @@ import java.awt.event.MouseEvent;
import java.util.*;
import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.TreePath;
import docking.ActionContext;
@ -32,8 +34,7 @@ import docking.widgets.tree.support.GTreeNodeTransferable;
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
import docking.widgets.tree.tasks.GTreeBulkTask;
import ghidra.app.plugin.core.symboltree.actions.*;
import ghidra.app.plugin.core.symboltree.nodes.SymbolNode;
import ghidra.app.plugin.core.symboltree.nodes.SymbolTreeRootNode;
import ghidra.app.plugin.core.symboltree.nodes.*;
import ghidra.framework.model.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.ComponentProviderAdapter;
@ -176,11 +177,31 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
}
});
newTree.addTreeExpansionListener(new TreeExpansionListener() {
@Override
public void treeExpanded(TreeExpansionEvent event) {
// nothing
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
treeNodeCollapsed(event.getPath());
}
});
newTree.setEditable(true);
return newTree;
}
protected void treeNodeCollapsed(TreePath path) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof SymbolCategoryNode) {
tree.runTask(m -> ((SymbolCategoryNode) lastPathComponent).unloadChildren());
}
}
private void maybeGoToSymbol() {
TreePath[] paths = tree.getSelectionPaths();

View file

@ -20,7 +20,9 @@ import java.util.*;
import javax.swing.Icon;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.tasks.GTreeCollapseAllTask;
import ghidra.program.model.symbol.Namespace;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -237,6 +239,24 @@ public class OrganizationNode extends SymbolTreeNode {
}
addNode(index, newNode);
checkForTooManyNodes();
}
private void checkForTooManyNodes() {
if (getChildCount() > SymbolCategoryNode.MAX_NODES_BEFORE_CLOSING) {
// If we have too many nodes, find the root category node and close it
GTreeNode parent = getParent();
while (parent != null) {
if (parent instanceof SymbolCategoryNode) {
GTree tree = getTree();
// also clear the selection so that it doesn't reopen the category needlessly
tree.clearSelectionPaths();
tree.runTask(new GTreeCollapseAllTask(tree, parent));
return;
}
parent = parent.getParent();
}
}
}
@Override
@ -310,6 +330,10 @@ public class OrganizationNode extends SymbolTreeNode {
moreNode = null;
}
super.removeNode(node);
// if this org node is empty, just remove it
if (getChildCount() == 0) {
getParent().removeNode(this);
}
}
@Override

View file

@ -18,7 +18,9 @@ package ghidra.app.plugin.core.symboltree.nodes;
import java.awt.datatransfer.DataFlavor;
import java.util.*;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.tasks.GTreeCollapseAllTask;
import ghidra.app.plugin.core.symboltree.SymbolCategory;
import ghidra.program.model.address.GlobalNamespace;
import ghidra.program.model.listing.Program;
@ -28,7 +30,9 @@ import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
public abstract class SymbolCategoryNode extends SymbolTreeNode {
private static final int MAX_NODES = 40;
public static final int MAX_NODES_BEFORE_ORGANIZING = 40;
public static final int MAX_NODES_BEFORE_CLOSING = 200;
protected SymbolCategory symbolCategory;
protected SymbolTable symbolTable;
protected GlobalNamespace globalNamespace;
@ -54,7 +58,7 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
SymbolType symbolType = symbolCategory.getSymbolType();
List<GTreeNode> list = getSymbols(symbolType, monitor);
monitor.checkCanceled();
return OrganizationNode.organize(list, MAX_NODES, monitor);
return OrganizationNode.organize(list, MAX_NODES_BEFORE_ORGANIZING, monitor);
}
public Program getProgram() {
@ -208,6 +212,14 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
}
parentNode.addNode(index, newNode);
if (parentNode.isLoaded() && parentNode.getChildCount() > MAX_NODES_BEFORE_CLOSING) {
GTree tree = parentNode.getTree();
// tree needs to be reorganized, close this category node to clear its children
// and force a reorganization next time it is opened
// also need to clear the selection so that it doesn't re-open the category
tree.clearSelectionPaths();
tree.runTask(new GTreeCollapseAllTask(tree, parentNode));
}
}
public void symbolRemoved(Symbol symbol, TaskMonitor monitor) {

View file

@ -40,9 +40,12 @@ import generic.test.AbstractGenericTest;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.marker.MarkerManagerPlugin;
import ghidra.app.plugin.core.programtree.ProgramTreePlugin;
import ghidra.app.plugin.core.symboltree.nodes.SymbolCategoryNode;
import ghidra.app.plugin.core.symboltree.nodes.SymbolNode;
import ghidra.app.services.ProgramManager;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@ -107,6 +110,40 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
env.dispose();
}
@Test
public void testCloseCategoryIfOrgnodesGetOutOfBalance() throws Exception {
showSymbolTree();
GTreeNode functionsNode = rootNode.getChild("Functions");
assertFalse(functionsNode.isLoaded());
functionsNode.expand();
waitForTree(tree);
assertTrue(functionsNode.isLoaded());
// add lots of nodes to cause functionsNode to close
addFunctions(SymbolCategoryNode.MAX_NODES_BEFORE_CLOSING);
waitForTree(tree);
assertFalse(functionsNode.isLoaded());
functionsNode.expand();
waitForTree(tree);
// should have 4 nodes, one for each of the original 3 functions and a org node with
// all new "FUNCTION*" named functions
assertEquals(4, functionsNode.getChildCount());
}
private void addFunctions(int count) throws Exception {
tx(program, () -> {
for (int i = 0; i < count; i++) {
String name = "FUNCTION_" + i;
Address address = util.addr(0x1002000 + i);
AddressSet body = new AddressSet(address);
program.getListing().createFunction(name, address, body, SourceType.USER_DEFINED);
}
});
}
@Test
public void testShowDisplay() throws Exception {
showSymbolTree();

View file

@ -24,8 +24,8 @@ import org.junit.Test;
import docking.test.AbstractDockingTest;
import docking.widgets.tree.GTreeNode;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.StubSymbol;
import ghidra.program.model.symbol.Symbol;
import ghidra.util.Swing;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
@ -160,7 +160,40 @@ public class OrganizationNodeTest extends AbstractDockingTest {
assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
assertEquals("12 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
}
@Test
public void testEmptyNodeIsRemoved() {
List<GTreeNode> nodeList = nodes("AA1", "AA2", "AA3", "AB1", "AB2", "AB3",
"BB1", "BB2", "BB3", "CCC", "DDD");
List<GTreeNode> result = organize(nodeList, 3);
// the result should have 4 nodes, the first being the "A" node
assertEquals(4, result.size());
GTreeNode nodeA = result.get(0);
assertEquals("A", nodeA.getName());
// The A node should have 2 children AA and AB
assertEquals(2, nodeA.getChildCount());
GTreeNode nodeAA = nodeA.getChild(0);
assertEquals("AA", nodeAA.getName());
// finally the AA node should have 3 children AA1,AA2,AA3
assertEquals(3, nodeAA.getChildCount());
GTreeNode nodeAA1 = nodeAA.getChild(0);
GTreeNode nodeAA2 = nodeAA.getChild(1);
GTreeNode nodeAA3 = nodeAA.getChild(2);
assertEquals("AA1", nodeAA1.getName());
assertEquals("AA2", nodeAA2.getName());
assertEquals("AA3", nodeAA3.getName());
// remove AA1,AA2,AA3, verify that AA is removed as well
nodeAA.removeNode(nodeAA1);
nodeAA.removeNode(nodeAA2);
nodeAA.removeNode(nodeAA3);
assertEquals(1, nodeA.getChildCount());
assertEquals("AB", nodeA.getChild(0).getName());
}
private void simulateSmbolDeleted(SymbolTreeNode root, Symbol symbolToDelete) {
@ -189,5 +222,4 @@ public class OrganizationNodeTest extends AbstractDockingTest {
private GTreeNode node(String name) {
return new CodeSymbolNode(null, new StubSymbol(name, null));
}
}