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>
|
</LI>
|
||||||
</UL>
|
</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
|
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
|
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
|
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
|
<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>
|
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>
|
<BLOCKQUOTE>
|
||||||
<P align="left"><IMG alt="" src="../../shared/note.png"> The Symbol Tree does not show
|
<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 java.util.*;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.TreeExpansionEvent;
|
||||||
|
import javax.swing.event.TreeExpansionListener;
|
||||||
import javax.swing.tree.TreePath;
|
import javax.swing.tree.TreePath;
|
||||||
|
|
||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
|
@ -32,8 +34,7 @@ import docking.widgets.tree.support.GTreeNodeTransferable;
|
||||||
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
|
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
|
||||||
import docking.widgets.tree.tasks.GTreeBulkTask;
|
import docking.widgets.tree.tasks.GTreeBulkTask;
|
||||||
import ghidra.app.plugin.core.symboltree.actions.*;
|
import ghidra.app.plugin.core.symboltree.actions.*;
|
||||||
import ghidra.app.plugin.core.symboltree.nodes.SymbolNode;
|
import ghidra.app.plugin.core.symboltree.nodes.*;
|
||||||
import ghidra.app.plugin.core.symboltree.nodes.SymbolTreeRootNode;
|
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.options.SaveState;
|
import ghidra.framework.options.SaveState;
|
||||||
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
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);
|
newTree.setEditable(true);
|
||||||
|
|
||||||
return newTree;
|
return newTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void treeNodeCollapsed(TreePath path) {
|
||||||
|
Object lastPathComponent = path.getLastPathComponent();
|
||||||
|
if (lastPathComponent instanceof SymbolCategoryNode) {
|
||||||
|
tree.runTask(m -> ((SymbolCategoryNode) lastPathComponent).unloadChildren());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void maybeGoToSymbol() {
|
private void maybeGoToSymbol() {
|
||||||
|
|
||||||
TreePath[] paths = tree.getSelectionPaths();
|
TreePath[] paths = tree.getSelectionPaths();
|
||||||
|
|
|
@ -20,7 +20,9 @@ import java.util.*;
|
||||||
|
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
|
|
||||||
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
|
import docking.widgets.tree.tasks.GTreeCollapseAllTask;
|
||||||
import ghidra.program.model.symbol.Namespace;
|
import ghidra.program.model.symbol.Namespace;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
@ -237,6 +239,24 @@ public class OrganizationNode extends SymbolTreeNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
addNode(index, newNode);
|
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
|
@Override
|
||||||
|
@ -310,6 +330,10 @@ public class OrganizationNode extends SymbolTreeNode {
|
||||||
moreNode = null;
|
moreNode = null;
|
||||||
}
|
}
|
||||||
super.removeNode(node);
|
super.removeNode(node);
|
||||||
|
// if this org node is empty, just remove it
|
||||||
|
if (getChildCount() == 0) {
|
||||||
|
getParent().removeNode(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -18,7 +18,9 @@ package ghidra.app.plugin.core.symboltree.nodes;
|
||||||
import java.awt.datatransfer.DataFlavor;
|
import java.awt.datatransfer.DataFlavor;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
|
import docking.widgets.tree.tasks.GTreeCollapseAllTask;
|
||||||
import ghidra.app.plugin.core.symboltree.SymbolCategory;
|
import ghidra.app.plugin.core.symboltree.SymbolCategory;
|
||||||
import ghidra.program.model.address.GlobalNamespace;
|
import ghidra.program.model.address.GlobalNamespace;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
|
@ -28,7 +30,9 @@ import ghidra.util.task.TaskMonitor;
|
||||||
import ghidra.util.task.TaskMonitorAdapter;
|
import ghidra.util.task.TaskMonitorAdapter;
|
||||||
|
|
||||||
public abstract class SymbolCategoryNode extends SymbolTreeNode {
|
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 SymbolCategory symbolCategory;
|
||||||
protected SymbolTable symbolTable;
|
protected SymbolTable symbolTable;
|
||||||
protected GlobalNamespace globalNamespace;
|
protected GlobalNamespace globalNamespace;
|
||||||
|
@ -54,7 +58,7 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
|
||||||
SymbolType symbolType = symbolCategory.getSymbolType();
|
SymbolType symbolType = symbolCategory.getSymbolType();
|
||||||
List<GTreeNode> list = getSymbols(symbolType, monitor);
|
List<GTreeNode> list = getSymbols(symbolType, monitor);
|
||||||
monitor.checkCanceled();
|
monitor.checkCanceled();
|
||||||
return OrganizationNode.organize(list, MAX_NODES, monitor);
|
return OrganizationNode.organize(list, MAX_NODES_BEFORE_ORGANIZING, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Program getProgram() {
|
public Program getProgram() {
|
||||||
|
@ -208,6 +212,14 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
parentNode.addNode(index, newNode);
|
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) {
|
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.codebrowser.CodeBrowserPlugin;
|
||||||
import ghidra.app.plugin.core.marker.MarkerManagerPlugin;
|
import ghidra.app.plugin.core.marker.MarkerManagerPlugin;
|
||||||
import ghidra.app.plugin.core.programtree.ProgramTreePlugin;
|
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.plugin.core.symboltree.nodes.SymbolNode;
|
||||||
import ghidra.app.services.ProgramManager;
|
import ghidra.app.services.ProgramManager;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
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.listing.Program;
|
||||||
import ghidra.program.model.symbol.*;
|
import ghidra.program.model.symbol.*;
|
||||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
|
@ -107,6 +110,40 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
|
||||||
env.dispose();
|
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
|
@Test
|
||||||
public void testShowDisplay() throws Exception {
|
public void testShowDisplay() throws Exception {
|
||||||
showSymbolTree();
|
showSymbolTree();
|
||||||
|
|
|
@ -24,8 +24,8 @@ import org.junit.Test;
|
||||||
|
|
||||||
import docking.test.AbstractDockingTest;
|
import docking.test.AbstractDockingTest;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
import ghidra.program.model.symbol.Symbol;
|
|
||||||
import ghidra.program.model.symbol.StubSymbol;
|
import ghidra.program.model.symbol.StubSymbol;
|
||||||
|
import ghidra.program.model.symbol.Symbol;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
|
@ -160,7 +160,40 @@ public class OrganizationNodeTest extends AbstractDockingTest {
|
||||||
|
|
||||||
assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
|
assertEquals(OrganizationNode.MAX_SAME_NAME + 1, dupNode.getChildCount());
|
||||||
assertEquals("12 more...", dupNode.getChild(OrganizationNode.MAX_SAME_NAME).getName());
|
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) {
|
private void simulateSmbolDeleted(SymbolTreeNode root, Symbol symbolToDelete) {
|
||||||
|
@ -189,5 +222,4 @@ public class OrganizationNodeTest extends AbstractDockingTest {
|
||||||
private GTreeNode node(String name) {
|
private GTreeNode node(String name) {
|
||||||
return new CodeSymbolNode(null, new StubSymbol(name, null));
|
return new CodeSymbolNode(null, new StubSymbol(name, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ public class FilterTextField extends JPanel {
|
||||||
@Override
|
@Override
|
||||||
public void setEnabled(boolean enabled) {
|
public void setEnabled(boolean enabled) {
|
||||||
textField.setEnabled(enabled);
|
textField.setEnabled(enabled);
|
||||||
updateField();
|
updateField(textField.getText().length() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -324,7 +324,7 @@ public class FilterTextField extends JPanel {
|
||||||
return textField;
|
return textField;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateField() {
|
private void updateField(boolean fireEvent) {
|
||||||
String text = getText();
|
String text = getText();
|
||||||
hasText = text.length() > 0;
|
hasText = text.length() > 0;
|
||||||
|
|
||||||
|
@ -332,7 +332,9 @@ public class FilterTextField extends JPanel {
|
||||||
|
|
||||||
updateBackgroundColor();
|
updateBackgroundColor();
|
||||||
|
|
||||||
fireFilterChanged(text);
|
if (fireEvent) {
|
||||||
|
fireFilterChanged(text);
|
||||||
|
}
|
||||||
|
|
||||||
boolean showFilterButton = hasText && textField.isEnabled();
|
boolean showFilterButton = hasText && textField.isEnabled();
|
||||||
updateFilterButton(showFilterButton);
|
updateFilterButton(showFilterButton);
|
||||||
|
@ -402,17 +404,17 @@ public class FilterTextField extends JPanel {
|
||||||
private class FilterDocumentListener implements DocumentListener {
|
private class FilterDocumentListener implements DocumentListener {
|
||||||
@Override
|
@Override
|
||||||
public void changedUpdate(DocumentEvent e) {
|
public void changedUpdate(DocumentEvent e) {
|
||||||
updateField();
|
updateField(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void insertUpdate(DocumentEvent e) {
|
public void insertUpdate(DocumentEvent e) {
|
||||||
updateField();
|
updateField(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeUpdate(DocumentEvent e) {
|
public void removeUpdate(DocumentEvent e) {
|
||||||
updateField();
|
updateField(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* ###
|
/* ###
|
||||||
* IP: GHIDRA
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,13 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package docking.widgets.tree.tasks;
|
package docking.widgets.tree.tasks;
|
||||||
|
|
||||||
import ghidra.util.exception.CancelledException;
|
|
||||||
import ghidra.util.task.TaskMonitor;
|
|
||||||
|
|
||||||
import javax.swing.JTree;
|
import javax.swing.JTree;
|
||||||
|
|
||||||
import docking.widgets.tree.GTree;
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeTask;
|
import docking.widgets.tree.GTreeTask;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class GTreeClearSelectionTask extends GTreeTask {
|
public class GTreeClearSelectionTask extends GTreeTask {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue