mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GP-1193 Improved behavior of symbol tree when bulk operations add/delete/rename large numbers of symbols
This commit is contained in:
parent
5e5d474279
commit
bf27b8e410
8 changed files with 149 additions and 17 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -277,7 +277,7 @@ public class FilterTextField extends JPanel {
|
|||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
textField.setEnabled(enabled);
|
||||
updateField();
|
||||
updateField(textField.getText().length() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -324,7 +324,7 @@ public class FilterTextField extends JPanel {
|
|||
return textField;
|
||||
}
|
||||
|
||||
private void updateField() {
|
||||
private void updateField(boolean fireEvent) {
|
||||
String text = getText();
|
||||
hasText = text.length() > 0;
|
||||
|
||||
|
@ -332,7 +332,9 @@ public class FilterTextField extends JPanel {
|
|||
|
||||
updateBackgroundColor();
|
||||
|
||||
if (fireEvent) {
|
||||
fireFilterChanged(text);
|
||||
}
|
||||
|
||||
boolean showFilterButton = hasText && textField.isEnabled();
|
||||
updateFilterButton(showFilterButton);
|
||||
|
@ -402,17 +404,17 @@ public class FilterTextField extends JPanel {
|
|||
private class FilterDocumentListener implements DocumentListener {
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
updateField();
|
||||
updateField(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
updateField();
|
||||
updateField(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
updateField();
|
||||
updateField(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,13 +15,12 @@
|
|||
*/
|
||||
package docking.widgets.tree.tasks;
|
||||
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
import javax.swing.JTree;
|
||||
|
||||
import docking.widgets.tree.GTree;
|
||||
import docking.widgets.tree.GTreeTask;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class GTreeClearSelectionTask extends GTreeTask {
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue