GP-5474 - Symbol Tree - Event handling improvements to maintain user view position; added an option for org node group threshold; Fixed missing nodes under classes

This commit is contained in:
dragonmacher 2025-03-25 14:14:03 -04:00
parent 397a814f5f
commit 5f17963eba
21 changed files with 1053 additions and 157 deletions

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -114,7 +114,7 @@ public class DisconnectedSymbolTreeProvider extends SymbolTreeProvider {
@Override
protected SymbolTreeRootNode createRootNode() {
return new ConfigurableSymbolTreeRootNode(program);
return new ConfigurableSymbolTreeRootNode(program, getNodeGroupThreshold());
}
@Override

View file

@ -22,12 +22,14 @@ import ghidra.app.CorePluginPackage;
import ghidra.app.events.*;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.GoToService;
import ghidra.framework.options.SaveState;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.*;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.bean.opteditor.OptionsVetoException;
//@formatter:off
@PluginInfo(
@ -36,8 +38,8 @@ import ghidra.program.util.ProgramLocation;
category = PluginCategoryNames.COMMON,
shortDescription = "Symbol Tree",
description = "This plugin shows the symbols from the program " +
"in a tree hierarchy. All symbols (except for the global namespace symbol)" +
" have a parent symbol. From the tree, symbols can be renamed, deleted, or " +
"in a tree hierarchy. All symbols (except for the global namespace symbol) " +
"have a parent symbol. From the tree, symbols can be renamed, deleted, or " +
"reorganized.",
eventsConsumed = { ProgramActivatedPluginEvent.class, ProgramLocationPluginEvent.class, ProgramClosedPluginEvent.class },
servicesProvided = { SymbolTreeService.class }
@ -45,7 +47,8 @@ import ghidra.program.util.ProgramLocation;
//@formatter:on
public class SymbolTreePlugin extends Plugin implements SymbolTreeService {
public static final String PLUGIN_NAME = "SymbolTreePlugin";
private static final String OPTIONS_CATEGORY = "Symbol Tree";
private static final String OPTION_NAME_GROUP_THRESHOLD = "Group Threshold";
private SymbolTreeProvider connectedProvider;
private List<SymbolTreeProvider> disconnectedProviders = new ArrayList<>();
@ -53,8 +56,12 @@ public class SymbolTreePlugin extends Plugin implements SymbolTreeService {
private GoToService goToService;
private boolean processingGoTo;
private OptionsChangeListener optionsListener = new SymbolTreeOptionsListener();
private int nodeGroupThreshold = 200;
public SymbolTreePlugin(PluginTool tool) {
super(tool);
connectedProvider = new SymbolTreeProvider(tool, this);
}
@ -108,6 +115,20 @@ public class SymbolTreePlugin extends Plugin implements SymbolTreeService {
@Override
protected void init() {
goToService = tool.getService(GoToService.class);
initializeOptions();
}
private void initializeOptions() {
ToolOptions options = tool.getOptions(OPTIONS_CATEGORY);
options.addOptionsChangeListener(optionsListener);
HelpLocation help = new HelpLocation("SymbolTreePlugin", "GroupNode");
options.registerOption(OPTION_NAME_GROUP_THRESHOLD, nodeGroupThreshold, help,
"The max number of children before nodes are organized by name");
nodeGroupThreshold = options.getInt(OPTION_NAME_GROUP_THRESHOLD, nodeGroupThreshold);
}
@Override
@ -192,4 +213,25 @@ public class SymbolTreePlugin extends Plugin implements SymbolTreeService {
connectedProvider.selectSymbol(symbol);
}
int getNodeGroupThreshold() {
return nodeGroupThreshold;
}
private class SymbolTreeOptionsListener implements OptionsChangeListener {
@Override
public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
Object newValue) throws OptionsVetoException {
if (OPTION_NAME_GROUP_THRESHOLD.equals(optionName)) {
nodeGroupThreshold = (int) newValue;
connectedProvider.rebuildTree();
for (SymbolTreeProvider provider : disconnectedProviders) {
provider.rebuildTree();
}
}
}
}
}

View file

@ -75,31 +75,34 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
* prevent to much work from happening too fast. Also, we perform the work in a bulk task
* so that the tree can benefit from optimizations made by the bulk task.
*/
private List<GTreeTask> bufferedTasks = new ArrayList<>();
private List<AbstractSymbolUpdateTask> bufferedTasks = new ArrayList<>();
private Map<Program, GTreeState> treeStateMap = new HashMap<>();
private SwingUpdateManager domainChangeUpdateManager = new SwingUpdateManager(1000,
AbstractSwingUpdateManager.DEFAULT_MAX_DELAY, "Symbol Tree Provider", () -> {
AbstractSwingUpdateManager.DEFAULT_MAX_DELAY, "Symbol Tree Provider - Bulk Update", () -> {
if (bufferedTasks.isEmpty()) {
return;
}
if (bufferedTasks.size() == 1) {
//
// Single events happen from user operations, like creating namespaces and
// rename operations.
//
// Perform a simple update in the normal fashion (a single, targeted filter
// performed when adding changing one symbol is faster than the complete
// refilter done by the bulk task below).
//
tree.runTask(bufferedTasks.remove(0));
List<AbstractSymbolUpdateTask> copiedTasks = new ArrayList<>(bufferedTasks);
bufferedTasks.clear();
tree.runTask(new BulkWorkTask(tree, copiedTasks));
});
// Track the tree state from before undo/redo operations so we can put the user's view back. We
// buffer this so the user can perform multiple rapid operations without responding to each one.
private GTreeState preRestoreTreeState;
private SwingUpdateManager restoredUpdateManager = new SwingUpdateManager(750,
AbstractSwingUpdateManager.DEFAULT_MAX_DELAY, "Symbol Tree Provider - Restore", () -> {
// trigger a delayed refilter to happen after we restore the tree state
tree.refilterLater();
if (preRestoreTreeState == null) {
return;
}
ArrayList<GTreeTask> copiedTasks = new ArrayList<>(bufferedTasks);
bufferedTasks.clear();
tree.runTask(new BulkWorkTask(tree, copiedTasks));
tree.restoreTreeState(preRestoreTreeState);
preRestoreTreeState = null;
});
public SymbolTreeProvider(PluginTool tool, SymbolTreePlugin plugin) {
@ -134,7 +137,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
}
protected SymbolTreeRootNode createRootNode() {
return new SymbolTreeRootNode(program);
return new SymbolTreeRootNode(program, getNodeGroupThreshold());
}
private JComponent buildProvider() {
@ -307,6 +310,10 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
// Class Methods
//==================================================================================================
protected int getNodeGroupThreshold() {
return plugin.getNodeGroupThreshold();
}
GTree getTree() {
return tree;
}
@ -467,9 +474,15 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
// seems safer to cancel an edit rather than to commit it without asking.
tree.cancelEditing();
// preserve the original state to handle multiple undo/redo requests
if (preRestoreTreeState == null) {
preRestoreTreeState = tree.getTreeState();
}
restoredUpdateManager.updateLater();
SymbolTreeRootNode node = (SymbolTreeRootNode) tree.getModelRoot();
node.setChildren(null);
tree.refilterLater();
}
private void symbolChanged(Symbol symbol) {
@ -488,7 +501,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
addTask(new SymbolRemovedTask(tree, symbol));
}
private void addTask(GTreeTask task) {
private void addTask(AbstractSymbolUpdateTask task) {
// Note: if we want to call this method from off the Swing thread, then we have to
// synchronize on the list that we are adding to here.
Swing.assertSwingThread("Adding tasks must be done on the Swing thread," +
@ -621,6 +634,11 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
private void processSymbolChanged(ProgramChangeRecord pcr) {
Symbol symbol = (Symbol) pcr.getObject();
Object oldValue = pcr.getOldValue();
if (oldValue instanceof Namespace oldNs) {
addTask(new SymbolScopeChangedTask(tree, symbol, oldNs));
return;
}
symbolChanged(symbol);
}
@ -712,12 +730,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
TreePath[] selectionPaths = tree.getSelectionPaths();
doRun(monitor);
if (selectionPaths.length != 0) {
tree.setSelectionPaths(selectionPaths);
}
}
@Override
@ -739,7 +752,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
// the symbol may have been deleted while we are processing bulk changes
if (!symbol.isDeleted()) {
GTreeNode newNode = rootNode.symbolAdded(symbol);
GTreeNode newNode = rootNode.symbolAdded(symbol, monitor);
tree.refilterLater(newNode);
}
}
@ -762,9 +775,32 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
// the symbol may have been deleted while we are processing bulk changes
if (!symbol.isDeleted()) {
root.symbolAdded(symbol);
SymbolNode newNode = root.symbolAdded(symbol, monitor);
tree.refilterLater(newNode);
}
}
}
private class SymbolScopeChangedTask extends AbstractSymbolUpdateTask {
private Namespace oldNamespace;
SymbolScopeChangedTask(GTree tree, Symbol symbol, Namespace oldNamespace) {
super(tree, symbol);
this.oldNamespace = oldNamespace;
}
@Override
void doRun(TaskMonitor monitor) throws CancelledException {
SymbolTreeRootNode root = (SymbolTreeRootNode) tree.getModelRoot();
root.symbolRemoved(symbol, oldNamespace, monitor);
// the symbol may have been deleted while we are processing bulk changes
if (!symbol.isDeleted()) {
SymbolNode newNode = root.symbolAdded(symbol, monitor);
tree.refilterLater(newNode);
}
tree.refilterLater();
}
}
@ -778,19 +814,21 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
void doRun(TaskMonitor monitor) throws CancelledException {
SymbolTreeRootNode root = (SymbolTreeRootNode) tree.getModelRoot();
root.symbolRemoved(symbol, symbol.getName(), monitor);
tree.refilterLater();
// Note: turned this off; less tree flashing seems worth having a parent node still in
// the tree that doesn't belong
// tree.refilterLater();
}
}
private class BulkWorkTask extends GTreeBulkTask {
// somewhat arbitrary max amount of work to perform...at some point it is faster to
// just reload the tree
// somewhat arbitrary limit to individual tasks... too many, then reload the tree
private static final int MAX_TASK_COUNT = 1000;
private List<GTreeTask> tasks;
private List<AbstractSymbolUpdateTask> tasks;
BulkWorkTask(GTree gTree, List<GTreeTask> tasks) {
BulkWorkTask(GTree gTree, List<AbstractSymbolUpdateTask> tasks) {
super(gTree);
this.tasks = tasks;
}
@ -803,10 +841,14 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
return;
}
for (GTreeTask task : tasks) {
GTreeState state = tree.getTreeState();
for (AbstractSymbolUpdateTask task : tasks) {
monitor.checkCancelled();
task.run(monitor);
}
GTreeRestoreTreeStateTask task = new GTreeRestoreTreeStateTask(tree, state);
task.run(monitor);
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -17,12 +17,14 @@ package ghidra.app.plugin.core.symboltree.nodes;
import java.awt.datatransfer.DataFlavor;
import java.util.*;
import java.util.Map.Entry;
import javax.swing.Icon;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.app.plugin.core.symboltree.SymbolCategory;
import ghidra.app.util.NamespaceUtils;
import ghidra.program.model.listing.GhidraClass;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.*;
@ -56,7 +58,134 @@ public class ClassCategoryNode extends SymbolCategoryNode {
}
@Override
public SymbolNode symbolAdded(Symbol symbol) {
protected boolean supportsSymbol(Symbol symbol) {
SymbolType symbolType = symbol.getSymbolType();
if (symbolType == symbolCategory.getSymbolType()) {
return true;
}
// must be in a class at some level
Namespace parentNamespace = symbol.getParentNamespace();
while (parentNamespace != null && parentNamespace != globalNamespace) {
if (parentNamespace instanceof GhidraClass) {
return true;
}
parentNamespace = parentNamespace.getParentNamespace();
}
return false;
}
@Override
public GTreeNode findSymbolTreeNode(SymbolNode key, boolean loadChildren, TaskMonitor monitor) {
if ((!isLoaded() && !loadChildren) || monitor.isCancelled()) {
return null;
}
Symbol symbol = key.getSymbol();
Namespace parentNs = symbol.getParentNamespace();
if (parentNs == globalNamespace) {
// no need to search for the class in the tree; the class only lives at the top
return findNode(this, key, loadChildren, monitor);
}
// set getAllClassNodes() for a description of the map
Map<GTreeNode, List<Namespace>> classNodes =
getAllClassNodes(symbol, parentNs, loadChildren, monitor);
if (classNodes.isEmpty()) {
return null;
}
// since the symbol lives in all of these paths, just pick one in a consistent way
List<GTreeNode> keys = new ArrayList<>(classNodes.keySet());
Collections.sort(keys);
GTreeNode classNode = keys.get(0);
List<Namespace> parentPath = classNodes.get(classNode);
GTreeNode symbolParent =
getNamespaceNode(classNode, parentPath, loadChildren, monitor);
return findNode(symbolParent, key, loadChildren, monitor);
}
@Override
public void symbolRemoved(Symbol symbol, Namespace oldNamespace, TaskMonitor monitor) {
if (!isLoaded()) {
return;
}
if (!supportsSymbol(symbol)) {
return;
}
SymbolNode key = SymbolNode.createKeyNode(symbol, symbol.getName(), program);
Namespace parentNs = symbol.getParentNamespace();
if (parentNs == globalNamespace) {
// no need to search for the class in the tree; the class only lives at the top
GTreeNode symbolNode = findNode(this, key, false, monitor);
if (symbolNode != null) {
removeNode(symbolNode);
}
return;
}
// set getAllClassNodes() for a description of the map
Map<GTreeNode, List<Namespace>> classNodes =
getAllClassNodes(symbol, oldNamespace, monitor);
removeSymbol(key, classNodes, monitor);
}
@Override
public void symbolRemoved(Symbol symbol, String oldName, TaskMonitor monitor) {
if (!isLoaded()) {
return;
}
if (!supportsSymbol(symbol)) {
return;
}
SymbolNode key = SymbolNode.createKeyNode(symbol, oldName, program);
Namespace parentNs = symbol.getParentNamespace();
if (parentNs == globalNamespace) {
// no need to search for the class in the tree; the class only lives at the top
GTreeNode symbolNode = findNode(this, key, false, monitor);
if (symbolNode != null) {
removeNode(symbolNode);
}
return;
}
// set getAllClassNodes() for a description of the map
Map<GTreeNode, List<Namespace>> classNodes = getAllClassNodes(symbol, parentNs, monitor);
removeSymbol(key, classNodes, monitor);
}
private void removeSymbol(SymbolNode key, Map<GTreeNode, List<Namespace>> classNodes,
TaskMonitor monitor) {
Set<Entry<GTreeNode, List<Namespace>>> entries = classNodes.entrySet();
for (Entry<GTreeNode, List<Namespace>> entry : entries) {
if (monitor.isCancelled()) {
return;
}
// start with the the top-level class node and walk the namespace path to find the
// parent for the given symbol
GTreeNode classNode = entry.getKey();
List<Namespace> parentPath = entry.getValue();
GTreeNode symbolParent =
getNamespaceNode(classNode, parentPath, false, monitor);
GTreeNode symbolNode = findNode(symbolParent, key, false, monitor);
if (symbolParent != null) {
symbolParent.removeNode(symbolNode);
}
}
}
@Override
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
if (!isLoaded()) {
return null;
}
@ -66,18 +195,110 @@ public class ClassCategoryNode extends SymbolCategoryNode {
}
if (symbol.getSymbolType() == symbolCategory.getSymbolType()) {
return doAddSymbol(symbol, this); // add new Class symbol
doAddSymbol(symbol, this); // add new flat Class symbol
}
// see if the symbol is in a class namespace
Namespace parentNamespace = symbol.getParentNamespace();
Symbol namespaceSymbol = parentNamespace.getSymbol();
SymbolNode key = SymbolNode.createNode(namespaceSymbol, program);
GTreeNode parentNode = findSymbolTreeNode(key, false, TaskMonitor.DUMMY);
if (parentNode == null) {
return null;
// set getAllClassNodes() for a description of the map
SymbolNode lastNode = null;
Namespace parentNs = symbol.getParentNamespace();
Map<GTreeNode, List<Namespace>> classNodes = getAllClassNodes(symbol, parentNs, monitor);
Set<Entry<GTreeNode, List<Namespace>>> entries = classNodes.entrySet();
for (Entry<GTreeNode, List<Namespace>> entry : entries) {
// start with the the top-level class node and walk the namespace path to find the
// parent for the given symbol
GTreeNode classNode = entry.getKey();
List<Namespace> parentPath = entry.getValue();
GTreeNode symbolParent =
getNamespaceNode(classNode, parentPath, false, monitor);
if (symbolParent != null) {
lastNode = doAddSymbol(symbol, symbolParent);
}
}
return doAddSymbol(symbol, parentNode);
return lastNode;
}
/*
Uses the namespace path of the given symbol to create a mapping from this Classes category
node's top-level child classes to the path from that child node to the given symbol node.
This mapping allows us to find the symbol in multiple tree paths, such as in this example:
Classes
Class1
Label1
BarNs
Class2
Label2
Class2
Label2
In this tree, the Label2 symbol is in the tree twice. The mapping created by this method
will have have as keys both Class1 and Class2. Class1 will be mapped to Class1/BarNs/Class2
and Class2 will be mapped to Class2 (since it only has one namespace element).
This code is needed because this Classes category node will duplicate class nodes. It puts
each class at the top-level (as a flattened view) and then also includes each class under
any other parent class nodes.
*/
private Map<GTreeNode, List<Namespace>> getAllClassNodes(Symbol symbol, Namespace parentNs,
TaskMonitor monitor) {
return getAllClassNodes(symbol, parentNs, false, monitor);
}
private Map<GTreeNode, List<Namespace>> getAllClassNodes(Symbol symbol, Namespace parentNs,
boolean loadChildren, TaskMonitor monitor) {
List<Namespace> parents = NamespaceUtils.getNamespaceParts(parentNs);
Map<GTreeNode, List<Namespace>> classByPath = new HashMap<>();
findAllClassNodes(this, parents, classByPath, loadChildren, monitor);
return classByPath;
}
private void findAllClassNodes(GTreeNode searchNode, List<Namespace> namespaces,
Map<GTreeNode, List<Namespace>> results, boolean loadChildren, TaskMonitor monitor) {
if ((!searchNode.isLoaded() && !loadChildren) || monitor.isCancelled()) {
return;
}
if (namespaces.isEmpty()) {
return;
}
Namespace namespace = getNextClass(namespaces);
if (namespace == null) {
return;
}
Symbol nsSymbol = namespace.getSymbol();
SymbolNode key = SymbolNode.createKeyNode(nsSymbol, nsSymbol.getName(), program);
GTreeNode namespaceNode = findNode(searchNode, key, loadChildren, monitor);
if (namespaceNode == null) {
return; // we hit the last namespace
}
if (namespaceNode instanceof ClassSymbolNode) {
List<Namespace> currentPath = new ArrayList<>(namespaces);
currentPath.add(0, namespace);
results.put(namespaceNode, currentPath);
}
// move to the next namespace
findAllClassNodes(searchNode, namespaces, results, loadChildren, monitor);
}
private GhidraClass getNextClass(List<Namespace> namespaces) {
while (namespaces.size() > 0) {
Namespace ns = namespaces.remove(0);
if (ns instanceof GhidraClass) {
return (GhidraClass) ns;
}
}
return null;
}
@Override
@ -99,23 +320,4 @@ public class ClassCategoryNode extends SymbolCategoryNode {
Collections.sort(list, getChildrenComparator());
return list;
}
@Override
protected boolean supportsSymbol(Symbol symbol) {
SymbolType symbolType = symbol.getSymbolType();
if (symbolType == symbolCategory.getSymbolType()) {
return true;
}
// must be in a class at some level
Namespace parentNamespace = symbol.getParentNamespace();
while (parentNamespace != null && parentNamespace != globalNamespace) {
if (parentNamespace instanceof GhidraClass) {
return true;
}
parentNamespace = parentNamespace.getParentNamespace();
}
return false;
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -29,8 +29,8 @@ import ghidra.program.model.listing.Program;
*/
public class ConfigurableSymbolTreeRootNode extends SymbolTreeRootNode {
public ConfigurableSymbolTreeRootNode(Program program) {
super(program);
public ConfigurableSymbolTreeRootNode(Program program, int groupThreshold) {
super(program, groupThreshold);
}
public void transferSettings(ConfigurableSymbolTreeRootNode otherRoot) {

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -76,6 +76,12 @@ class ExportsCategoryNode extends SymbolCategoryNode {
if (!symbol.isPrimary()) {
return false;
}
return symbol.isExternalEntryPoint() || symbol.getParentSymbol().isExternalEntryPoint();
if (symbol.isExternalEntryPoint()) {
return true;
}
Symbol parent = symbol.getParentSymbol();
return parent != null && parent.isExternalEntryPoint();
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -102,7 +102,7 @@ class FunctionCategoryNode extends SymbolCategoryNode {
}
@Override
public SymbolNode symbolAdded(Symbol symbol) {
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
if (!isLoaded()) {
return null;
}
@ -111,15 +111,20 @@ class FunctionCategoryNode extends SymbolCategoryNode {
return null;
}
// only allow functions in the global namespace; others live in the Namespaces category
if (!isGlobalFunction(symbol)) {
return null;
}
// variables and parameters will be beneath function nodes, and our parent method
// will find them
if (isVariableParameterOrCodeSymbol(symbol)) {
return super.symbolAdded(symbol);
return super.symbolAdded(symbol, monitor);
}
// this namespace will be beneath function nodes, and our parent method will find them
if (isChildNamespaceOfFunction(symbol)) {
return super.symbolAdded(symbol);
return super.symbolAdded(symbol, monitor);
}
// ...otherwise, we have a function and we need to add it as a child of our parent node
@ -128,6 +133,15 @@ class FunctionCategoryNode extends SymbolCategoryNode {
return newNode;
}
private boolean isGlobalFunction(Symbol symbol) {
SymbolType type = symbol.getSymbolType();
if (type != SymbolType.FUNCTION) {
return false;
}
Namespace namespace = symbol.getParentNamespace();
return namespace == globalNamespace;
}
private boolean isChildNamespaceOfFunction(Symbol symbol) {
if (symbol instanceof Function) {
return false;
@ -151,7 +165,15 @@ class FunctionCategoryNode extends SymbolCategoryNode {
@Override
protected boolean supportsSymbol(Symbol symbol) {
if (super.supportsSymbol(symbol)) {
if (symbol.isExternal()) {
return false;
}
// Note: we say that we support function symbol types, even though we do not include
// functions that live in non-global namespaces. This allows functions that are moved to
// be removed from this category.
SymbolType type = symbol.getSymbolType();
if (type == SymbolType.FUNCTION) {
return true;
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -61,6 +61,15 @@ public class LabelCategoryNode extends SymbolCategoryNode {
return false;
}
@Override
protected boolean supportsSymbol(Symbol symbol) {
if (!symbol.isGlobal() || symbol.isExternal()) {
return false;
}
SymbolType symbolType = symbol.getSymbolType();
return symbolType == symbolCategory.getSymbolType();
}
@Override
protected List<GTreeNode> getSymbols(SymbolType type, TaskMonitor monitor)
throws CancelledException {
@ -85,7 +94,7 @@ public class LabelCategoryNode extends SymbolCategoryNode {
}
@Override
public SymbolNode symbolAdded(Symbol symbol) {
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
if (!isLoaded()) {
return null;
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,14 +16,17 @@
package ghidra.app.plugin.core.symboltree.nodes;
import java.awt.datatransfer.DataFlavor;
import java.util.List;
import javax.swing.Icon;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.app.plugin.core.symboltree.SymbolCategory;
import ghidra.app.util.NamespaceUtils;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.*;
import ghidra.util.task.TaskMonitor;
public class NamespaceCategoryNode extends SymbolCategoryNode {
@ -43,7 +46,13 @@ public class NamespaceCategoryNode extends SymbolCategoryNode {
@Override
protected boolean supportsSymbol(Symbol symbol) {
if (super.supportsSymbol(symbol)) {
if (symbol.isExternal()) {
return false;
}
SymbolType symbolType = symbol.getSymbolType();
if (symbolType == SymbolType.NAMESPACE) {
return true;
}
@ -52,6 +61,78 @@ public class NamespaceCategoryNode extends SymbolCategoryNode {
return parentNamespace != null && parentNamespace != globalNamespace;
}
@Override
public void symbolRemoved(Symbol symbol, Namespace oldNamespace, TaskMonitor monitor) {
if (!isLoaded()) {
return;
}
if (!supportsSymbol(symbol)) {
return;
}
List<Namespace> parents = NamespaceUtils.getNamespaceParts(oldNamespace);
GTreeNode namespaceNode = getNamespaceNode(this, parents, false, monitor);
if (namespaceNode == null) {
return;
}
SymbolNode key = SymbolNode.createKeyNode(symbol, symbol.getName(), program);
GTreeNode foundNode = findNode(namespaceNode, key, false, monitor);
if (foundNode == null) {
return;
}
GTreeNode foundParent = foundNode.getParent();
foundParent.removeNode(foundNode);
}
@Override
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
if (!isLoaded()) {
return null;
}
if (!supportsSymbol(symbol)) {
return null;
}
GTreeNode parentNode = this;
if (symbol.isGlobal()) {
return doAddSymbol(symbol, parentNode);
}
Namespace parentNamespace = symbol.getParentNamespace();
List<Namespace> parents = NamespaceUtils.getNamespaceParts(parentNamespace);
GTreeNode namespaceNode = getNamespaceNode(this, parents, false, monitor);
if (namespaceNode == null) {
return null;
}
return doAddSymbol(symbol, namespaceNode);
}
@Override
public GTreeNode findSymbolTreeNode(SymbolNode key, boolean loadChildren, TaskMonitor monitor) {
if ((!isLoaded() && !loadChildren) || monitor.isCancelled()) {
return null;
}
Symbol symbol = key.getSymbol();
Namespace parent = symbol.getParentNamespace();
List<Namespace> parents = NamespaceUtils.getNamespaceParts(parent);
GTreeNode namespaceNode = getNamespaceNode(this, parents, loadChildren, monitor);
if (namespaceNode != null) {
return findNode(namespaceNode, key, loadChildren, monitor);
}
// look in the namespace node for the given symbol
return findNode(this, key, loadChildren, monitor);
}
@Override
public boolean supportsDataFlavors(DataFlavor[] dataFlavors) {
for (DataFlavor flavor : dataFlavors) {

View file

@ -243,19 +243,24 @@ public class OrganizationNode extends SymbolTreeNode {
}
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();
SymbolTreeRootNode root = (SymbolTreeRootNode) getRoot();
int reOrgLimit = root.getReorganizeLimit();
if (getChildCount() < reOrgLimit) {
return;
}
// 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();
}
}

View file

@ -29,8 +29,6 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public abstract class SymbolCategoryNode extends SymbolTreeNode {
public static final int MAX_NODES_BEFORE_ORGANIZING = 100;
public static final int MAX_NODES_BEFORE_CLOSING = 200;
protected SymbolCategory symbolCategory;
protected SymbolTable symbolTable;
@ -76,7 +74,9 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
SymbolType symbolType = symbolCategory.getSymbolType();
List<GTreeNode> list = getSymbols(symbolType, monitor);
monitor.checkCancelled();
return OrganizationNode.organize(list, MAX_NODES_BEFORE_ORGANIZING, monitor);
SymbolTreeRootNode root = (SymbolTreeRootNode) getRoot();
int groupThreshold = root.getNodeGroupThreshold();
return OrganizationNode.organize(list, groupThreshold, monitor);
}
public Program getProgram() {
@ -173,7 +173,9 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
dataFlavor == ClassSymbolNode.LOCAL_DATA_FLAVOR;
}
public SymbolNode symbolAdded(Symbol symbol) {
protected abstract boolean supportsSymbol(Symbol symbol);
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
if (!isLoaded()) {
return null;
@ -191,7 +193,7 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
Namespace parentNamespace = symbol.getParentNamespace();
Symbol namespaceSymbol = parentNamespace.getSymbol();
SymbolNode key = SymbolNode.createNode(namespaceSymbol, program);
parentNode = findSymbolTreeNode(key, false, TaskMonitor.DUMMY);
parentNode = findSymbolTreeNode(key, false, monitor);
if (parentNode == null) {
return null;
}
@ -230,11 +232,17 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
}
parentNode.addNode(index, newNode);
if (parentNode.isLoaded() && parentNode.getChildCount() > MAX_NODES_BEFORE_CLOSING) {
if (!parentNode.isLoaded()) {
return;
}
SymbolTreeRootNode root = (SymbolTreeRootNode) getRoot();
int reOrgLimit = root.getReorganizeLimit();
if (parentNode.getChildCount() > reOrgLimit) {
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
// The 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));
}
@ -249,6 +257,10 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
return;
}
if (!supportsSymbol(symbol)) {
return;
}
SymbolNode key = SymbolNode.createKeyNode(symbol, oldName, program);
GTreeNode foundNode = findSymbolTreeNode(key, false, monitor);
if (foundNode == null) {
@ -259,12 +271,45 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode {
foundParent.removeNode(foundNode);
}
protected boolean supportsSymbol(Symbol symbol) {
if (!symbol.isGlobal() || symbol.isExternal()) {
return false;
public void symbolRemoved(Symbol symbol, Namespace oldNamespace, TaskMonitor monitor) {
// Most categories will treat a symbol moved as a remove; symbolAdded() will get called
// after this to restore the symbol. Subclasses that depend on scope will override this
// method.
symbolRemoved(symbol, monitor);
}
/**
* Returns the last Namespace tree node in the given path of namespaces. Each Namespace in the
* list from 0 to n will be used to find the last tree node, starting at the given parent
* node.
*
* @param parentNode the node at which to start the search
* @param namespaces the list of namespaces to traverse.
* @param loadChildren true to load children if they have not been loaded
* @param monitor the task monitor
* @return the namespace node or null if it is not open in the tree
*/
protected GTreeNode getNamespaceNode(GTreeNode parentNode, List<Namespace> namespaces,
boolean loadChildren, TaskMonitor monitor) {
if (!loadChildren && !parentNode.isLoaded() || monitor.isCancelled()) {
return null;
}
SymbolType symbolType = symbol.getSymbolType();
return symbolType == symbolCategory.getSymbolType();
if (namespaces.isEmpty()) {
return null;
}
Namespace namespace = namespaces.remove(0);
Symbol nsSymbol = namespace.getSymbol();
SymbolNode key = SymbolNode.createKeyNode(nsSymbol, nsSymbol.getName(), program);
GTreeNode namespaceNode = findNode(parentNode, key, loadChildren, monitor);
if (namespaceNode == null || namespaces.isEmpty()) {
return namespaceNode; // we hit the last namespace
}
// move to the next namespace
return getNamespaceNode(namespaceNode, namespaces, loadChildren, monitor);
}
@Override

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -38,6 +38,8 @@ public class SymbolNode extends SymbolTreeNode {
protected final Program program;
protected final Symbol symbol;
private String cachedName;
private boolean isCut;
SymbolNode(Program program, Symbol symbol) {
@ -113,8 +115,11 @@ public class SymbolNode extends SymbolTreeNode {
@Override
public String getName() {
String baseName = symbol.getName();
return getNameFromBaseName(baseName);
if (cachedName == null) {
String baseName = symbol.getName();
cachedName = getNameFromBaseName(baseName);
}
return cachedName;
}
protected String getNameFromBaseName(String baseName) {

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -176,18 +176,23 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode {
public GTreeNode findSymbolTreeNode(SymbolNode key, boolean loadChildren, TaskMonitor monitor) {
// if we don't have to loadChildren and we are not loaded get out.
if (!loadChildren && !isLoaded()) {
if ((!isLoaded() && !loadChildren) || monitor.isCancelled()) {
return null;
}
// see if the given node is the node we want
Symbol searchSymbol = key.getSymbol();
if (getSymbol() == searchSymbol) {
return this;
}
List<GTreeNode> children = getChildren();
int index = Collections.binarySearch(children, key, getChildrenComparator());
if (index >= 0) {
GTreeNode node = children.get(index);
SymbolTreeNode symbolNode = (SymbolTreeNode) node;
Symbol searchSymbol = key.getSymbol();
if (symbolNode.getSymbol() == searchSymbol) {
return node;
return symbolNode;
}
// At this point we know that the given child is not itself a symbol node, but it
@ -199,7 +204,9 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode {
return node;
}
// Brute-force lookup in each child. This will not typically be called.
// Brute-force lookup in each child. This will not typically be called. Category nodes
// that support large numbers of children have overridden this method to perform smarter
// searching.
for (GTreeNode childNode : children) {
if (monitor.isCancelled()) {
return null;
@ -217,4 +224,48 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode {
return null;
}
/**
* Searches for the given node 'key' inside of the given parent. This method performs an
* efficient search and does not recurse below the given node.
*
* @param parent the node whose children will be searched
* @param key the token node to search for
* @param loadChildren true to load children; false signals to search only if already loaded
* @param monitor the monitor
* @return the node or null
*/
protected GTreeNode findNode(GTreeNode parent, SymbolNode key, boolean loadChildren,
TaskMonitor monitor) {
if ((!isLoaded() && !loadChildren) || monitor.isCancelled()) {
return null;
}
// see if the given node is the node we want
Symbol searchSymbol = key.getSymbol();
if (parent instanceof SymbolTreeNode symbolNode) {
if (symbolNode.getSymbol() == searchSymbol) {
return symbolNode;
}
}
Comparator<GTreeNode> comparator = ((SymbolTreeNode) parent).getChildrenComparator();
List<GTreeNode> children = parent.getChildren();
int index = Collections.binarySearch(children, key, comparator);
if (index >= 0) {
GTreeNode node = children.get(index);
SymbolTreeNode symbolNode = (SymbolTreeNode) node;
// Some parent nodes may contain OrganizationNodes, which will return as a match when
// the symbol does not match. We expect the symbol to always match for clients of this
// method.
if (symbolNode.getSymbol() == searchSymbol) {
return symbolNode;
}
}
return null;
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -25,8 +25,7 @@ import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.app.plugin.core.symboltree.SymbolCategory;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolType;
import ghidra.program.model.symbol.*;
import ghidra.util.task.TaskMonitor;
public class SymbolTreeRootNode extends GTreeNode {
@ -35,8 +34,10 @@ public class SymbolTreeRootNode extends GTreeNode {
protected SymbolCategory symbolCategory;
protected Program program;
private int groupThreshold;
public SymbolTreeRootNode(Program program) {
public SymbolTreeRootNode(Program program, int groupThreshold) {
this.groupThreshold = groupThreshold;
this.symbolCategory = SymbolCategory.ROOT_CATEGORY;
this.program = program;
@ -48,6 +49,17 @@ public class SymbolTreeRootNode extends GTreeNode {
}
}
public int getNodeGroupThreshold() {
return groupThreshold;
}
public int getReorganizeLimit() {
// Arbitrary number to prevent bulk updates from triggering repeated node organization.
// The higher the value, the longer the delay between the tree collapsing nodes to signal
// that a re-organzation is needed.
return groupThreshold * 2;
}
public Program getProgram() {
return program;
}
@ -126,7 +138,6 @@ public class SymbolTreeRootNode extends GTreeNode {
}
private GTreeNode findClassSymbol(SymbolNode key, boolean loadChildren, TaskMonitor monitor) {
SymbolCategoryNode category = getClassesNode();
return category.findSymbolTreeNode(key, loadChildren, monitor);
}
@ -238,12 +249,12 @@ public class SymbolTreeRootNode extends GTreeNode {
return null; // must be filtered out
}
public SymbolNode symbolAdded(Symbol symbol) {
public SymbolNode symbolAdded(Symbol symbol, TaskMonitor monitor) {
SymbolNode returnNode = null;
List<GTreeNode> allChildren = getChildren();
for (GTreeNode gNode : allChildren) {
SymbolCategoryNode symbolNode = (SymbolCategoryNode) gNode;
SymbolNode newNode = symbolNode.symbolAdded(symbol);
SymbolNode newNode = symbolNode.symbolAdded(symbol, monitor);
if (newNode != null) {
returnNode = newNode; // doesn't matter which one we return
}
@ -261,6 +272,16 @@ public class SymbolTreeRootNode extends GTreeNode {
}
}
public void symbolRemoved(Symbol symbol, Namespace oldNamespace, TaskMonitor monitor) {
// we have to loop--the symbol may exist in more than one category
List<GTreeNode> allChildren = getChildren();
for (GTreeNode gNode : allChildren) {
SymbolCategoryNode symbolNode = (SymbolCategoryNode) gNode;
symbolNode.symbolRemoved(symbol, oldNamespace, monitor);
}
}
public void rebuild() {
setChildren(null);
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -50,6 +50,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.task.TaskMonitor;
/**
* Tests for the symbol tree plugin.
@ -118,7 +119,8 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
assertTrue(functionsNode.isLoaded());
// add lots of nodes to cause functionsNode to close
addFunctions(SymbolCategoryNode.MAX_NODES_BEFORE_CLOSING);
int reorganizeLimit = ((SymbolTreeRootNode) rootNode).getReorganizeLimit();
addFunctions(reorganizeLimit);
waitForTree(tree);
assertFalse(functionsNode.isLoaded());
@ -325,6 +327,7 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
flushAndWaitForTree();
// Functions node
GTreeNode fNode = rootNode.getChild(2);
util.expandNode(fNode);
@ -341,10 +344,13 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
assertTrue(cutAction.isEnabledForContext(util.getSymbolTreeContext()));
performAction(cutAction, util.getSymbolTreeContext(), true);
// NewNamespace node
GTreeNode gNode = namespaceNode.getChild(0);
util.selectNode(gNode);
assertTrue(pasteAction.isEnabledForContext(util.getSymbolTreeContext()));
// doStuff function node
waitForSwing();
GTreeNode dNode = fNode.getChild(0);
util.selectNode(dNode);
assertFalse(pasteAction.isEnabledForContext(util.getSymbolTreeContext()));
@ -775,7 +781,7 @@ public class SymbolTreePlugin1Test extends AbstractGhidraHeadedIntegrationTest {
Symbol symbol = fNode.getSymbol();
// symbolAdded() was throwing an exception before the fix
symbolRootNode.symbolAdded(symbol);
symbolRootNode.symbolAdded(symbol, TaskMonitor.DUMMY);
}
private void addFunctions(int count) throws Exception {

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -30,6 +30,8 @@ import docking.action.DockingActionIf;
import docking.action.ToggleDockingAction;
import docking.widgets.tree.GTreeNode;
import generic.test.AbstractGenericTest;
import ghidra.app.cmd.label.CreateNamespacesCmd;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.marker.MarkerManagerPlugin;
import ghidra.app.plugin.core.programtree.ProgramTreePlugin;
@ -38,8 +40,8 @@ import ghidra.app.plugin.core.symboltree.nodes.SymbolNode;
import ghidra.app.util.viewer.field.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.address.AddressFactory;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
@ -313,6 +315,217 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest {
waitForCondition(tree::isEditing);
}
@Test
public void testClassCategoryDuplicates_NestedClass_RenameLabel() throws Exception {
/*
The Classes folder flattens classes so every class appears at the top level. Because
users can expand classes, top level classes may also appear nested under other classes.
Classes
Class1
Label1
BarNs
Class2
Label2
Class2
Label2
Namespaces
FooNs
Class1
Label1
BarNs
Class2
Label2
*/
Namespace fooNs = createNamespace("FooNs");
GhidraClass class1 = createClass(fooNs, "Class1");
Namespace barNs = createNamespace(class1, "BarNs");
createLabel(class1, "Label1", "0x1001100");
GhidraClass class2 = createClass(barNs, "Class2");
Symbol lable2 = createLabel(class2, "Label2", "0x1001104");
expandClasses();
expandNamesapces();
// verify all leaf nodes
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::Label1",
"FooNs::Class1::BarNs::Class2::Label2"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1::Label1",
"Class1::BarNs::Class2::Label2",
"Class2::Label2"
);
//@formatter:on
renameSymbol(lable2, "Label2.renamed");
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::Label1",
"FooNs::Class1::BarNs::Class2::Label2.renamed"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1::Label1",
"Class1::BarNs::Class2::Label2.renamed",
"Class2::Label2.renamed"
);
//@formatter:on
}
@Test
public void testClassCategoryDuplicates_NestedClass_ChangeNamespace() throws Exception {
/*
The Classes folder flattens classes so every class appears at the top level. Because
users can expand classes, top level classes may also appear nested under other classes.
Classes
Class1
Label1
BarNs
Class2
Label2
Class2
Label2
Namespaces
FooNs
Class1
Label1
BarNs
Class2
Label2
*/
Namespace fooNs = createNamespace("FooNs");
GhidraClass class1 = createClass(fooNs, "Class1");
Namespace barNs = createNamespace(class1, "BarNs");
Symbol label1 = createLabel(class1, "Label1", "0x1001100");
GhidraClass class2 = createClass(barNs, "Class2");
createLabel(class2, "Label2", "0x1001104");
expandClasses();
expandNamesapces();
// verify all leaf nodes
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::Label1",
"FooNs::Class1::BarNs::Class2::Label2"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1::Label1",
"Class1::BarNs::Class2::Label2",
"Class2::Label2"
);
//@formatter:on
moveLabel(label1, barNs);
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::BarNs::Label1",
"FooNs::Class1::BarNs::Class2::Label2"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1",
"Class1::BarNs::Label1",
"Class1::BarNs::Class2::Label2",
"Class2::Label2"
);
//@formatter:on
}
@Test
public void testClassCategoryDuplicates_NestedClass_RenameNamespace() throws Exception {
/*
The Classes folder flattens classes so every class appears at the top level. Because
users can expand classes, top level classes may also appear nested under other classes.
Classes
Class1
Label1
BarNs
Class2
Label2
Class2
Label2
Namespaces
FooNs
Class1
Label1
BarNs
Class2
Label2
*/
Namespace fooNs = createNamespace("FooNs");
GhidraClass class1 = createClass(fooNs, "Class1");
Namespace barNs = createNamespace(class1, "BarNs");
createLabel(class1, "Label1", "0x1001100");
GhidraClass class2 = createClass(barNs, "Class2");
createLabel(class2, "Label2", "0x1001104");
expandClasses();
expandNamesapces();
// verify all leaf nodes
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::Label1",
"FooNs::Class1::BarNs::Class2::Label2"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1::Label1",
"Class1::BarNs::Class2::Label2",
"Class2::Label2"
);
//@formatter:on
renameNamespace(barNs, "BarNs.renamed");
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1::Label1",
"FooNs::Class1::BarNs.renamed::Class2::Label2"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1::Label1",
"Class1::BarNs.renamed::Class2::Label2",
"Class2::Label2"
);
//@formatter:on
}
@Test
public void testActionsOnGroup() throws Exception {
// select a group node; only cut, delete, make selection should be
@ -403,6 +616,100 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest {
assertEquals("MyAnotherLocal", (((SymbolNode) node).getSymbol()).getName());
}
//=================================================================================================
// Private Methods
//=================================================================================================
private void expandClasses() {
GTreeNode node = rootNode.getChild("Classes");
tree.expandTree(node);
waitForTree(tree);
}
private void expandNamesapces() {
GTreeNode node = rootNode.getChild("Namespaces");
tree.expandTree(node);
waitForTree(tree);
}
private void renameSymbol(Symbol s, String newName) {
RenameLabelCmd cmd = new RenameLabelCmd(s, newName, SourceType.USER_DEFINED);
if (!applyCmd(program, cmd)) {
fail("Rename failed: " + cmd.getStatusMsg());
}
waitForTree(tree);
}
private void moveLabel(Symbol symbol, Namespace ns) {
tx(program, () -> {
symbol.setNamespace(ns);
});
waitForTree(tree);
}
private void renameNamespace(Namespace barNs, String newName) {
Symbol symbol = barNs.getSymbol();
renameSymbol(symbol, newName);
}
private void assertNamespaceNodes(String... paths) {
GTreeNode root = tree.getViewRoot();
GTreeNode parent = root.getChild("Namespaces");
assertNodes(parent, paths);
}
private void assertClassNodes(String... paths) {
GTreeNode root = tree.getViewRoot();
GTreeNode parent = root.getChild("Classes");
assertNodes(parent, paths);
}
private void assertNodes(GTreeNode category, String... paths) {
for (String path : paths) {
GTreeNode parent = category;
String[] parts = path.split("::");
for (String name : parts) {
GTreeNode child = parent.getChild(name);
String message =
"Child '%s' not found in parent '%s' \n\tfor path '%s'\n\tCategory '%s'"
.formatted(name, parent, path, category);
assertNotNull(message, child);
parent = child;
}
}
}
private Namespace createNamespace(String name) throws Exception {
return createNamespace(program.getGlobalNamespace(), name);
}
private Namespace createNamespace(Namespace parent, String name) throws Exception {
CreateNamespacesCmd cmd = new CreateNamespacesCmd(name, parent, SourceType.USER_DEFINED);
applyCmd(program, cmd);
return cmd.getNamespace();
}
private GhidraClass createClass(Namespace parent, String name) throws Exception {
GhidraClass c = tx(program, () -> {
SymbolTable symbolTable = program.getSymbolTable();
return symbolTable.createClass(parent, name, SourceType.USER_DEFINED);
});
assertNotNull(c);
return c;
}
private Symbol createLabel(Namespace parent, String name, String addr) {
Symbol s = tx(program, () -> {
SymbolTable symbolTable = program.getSymbolTable();
AddressFactory af = program.getAddressFactory();
Address address = af.getAddress(addr);
return symbolTable.createLabel(address, name, parent, SourceType.USER_DEFINED);
});
assertNotNull(s);
return s;
}
private GTreeNode getFunctionsNode() {
return runSwing(() -> rootNode.getChild(2));
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -232,26 +232,26 @@ class SymbolTreeTestUtils {
selectNode(parenGTreeNode);
int childCount = parenGTreeNode.getChildCount();
int index = parenGTreeNode.getIndexInParent();
GTreeNode pNode = parenGTreeNode.getParent();
int parentIndex = parenGTreeNode.getIndexInParent();
GTreeNode grandParentNode = parenGTreeNode.getParent();
AbstractDockingTest.performAction(action, getSymbolTreeContext(), false);
waitForSwing();
waitForTree();
program.flushEvents();
if (pNode != null) {
// re-acquire parent
parenGTreeNode = pNode.getChild(index);
if (grandParentNode != null) {
parenGTreeNode = grandParentNode.getChild(parentIndex);
}
GTreeNode node = parenGTreeNode.getChild(childCount > 0 ? childCount - 1 : 0);
GTreeNode newNode = parenGTreeNode.getChild(childCount > 0 ? childCount - 1 : 0);
waitForTree();
runSwing(() -> tree.stopEditing());
waitForCondition(() -> !tree.isEditing());
rename(node, newName);
rename(newNode, newName);
return parenGTreeNode.getChild(newName);
}
@ -274,6 +274,11 @@ class SymbolTreeTestUtils {
waitForTree();
}
void expandTree() {
tree.expandAll();
waitForTree();
}
void expandNode(GTreeNode parenGTreeNode) throws Exception {
tree.expandPath(parenGTreeNode);
waitForTree();

View file

@ -2144,6 +2144,11 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
TimeUnit.NANOSECONDS));
*/
doWaitForTree(gTree);
// some client tree operations will launch tasks that wait for the tree and then call a
// Swing task to run at some point after that. waitForSwing() is not good enough for these,
// since the tree may be using a timer that has not yet expired.
waitForExpiringSwingTimers();
}
private static void doWaitForTree(GTree gTree) {

View file

@ -597,7 +597,6 @@ public class GTree extends JPanel implements BusyListener {
* @param origin the event type; use {@link EventOrigin#API_GENERATED} if unsure
*/
public void setSelectionPaths(List<TreePath> paths, boolean expandPaths, EventOrigin origin) {
if (expandPaths) {
expandPaths(paths);
}
@ -1432,7 +1431,7 @@ public class GTree extends JPanel implements BusyListener {
*/
public void refilterLater() {
if (isFilteringEnabled && filter != null) {
filterUpdateManager.update();
filterUpdateManager.updateLater();
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -15,7 +15,7 @@
*/
package generic.timer;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.*;
@ -32,6 +32,8 @@ import utility.function.Dummy;
*/
public class ExpiringSwingTimer extends GhidraSwingTimer {
private static Set<ExpiringSwingTimer> instances = new HashSet<>();
private long startMs = System.currentTimeMillis();
private int expireMs;
private BooleanSupplier isReady;
@ -130,9 +132,16 @@ public class ExpiringSwingTimer extends GhidraSwingTimer {
return;
}
instances.add(this);
super.start();
}
@Override
public void stop() {
super.stop();
instances.remove(this);
}
/**
* Returns true the initial expiration period has passed
* @return true if expired

View file

@ -34,6 +34,7 @@ import javax.swing.tree.*;
import org.junit.Assert;
import generic.timer.ExpiringSwingTimer;
import ghidra.framework.ApplicationConfiguration;
import ghidra.util.*;
import ghidra.util.datastruct.WeakSet;
@ -1149,6 +1150,39 @@ public class AbstractGuiTest extends AbstractGenericTest {
// Swing Methods
//==================================================================================================
public static boolean waitForExpiringSwingTimers() {
if (SwingUtilities.isEventDispatchThread()) {
throw new AssertException("Can't wait for swing from within the swing thread!");
}
// Note: this is based on the waitForSwing() timeout; this can be adjusted
boolean waited = false;
int MAX_SWING_TIMEOUT = 15000;
int totalTime = 0;
while (totalTime < MAX_SWING_TIMEOUT) {
@SuppressWarnings("unchecked")
Set<ExpiringSwingTimer> timers = runSwing(() -> {
return (Set<ExpiringSwingTimer>) getInstanceField("instances",
ExpiringSwingTimer.class);
});
if (timers.isEmpty()) {
return waited;
}
waited = true;
totalTime += sleep(DEFAULT_WAIT_DELAY);
}
if (totalTime >= MAX_SWING_TIMEOUT) {
Msg.debug(AbstractGenericTest.class,
"Timed-out waitinig for ExpiringSwingTimerc after " + totalTime + " ms. ");
return true;
}
return true;
}
/**
* Waits for the Swing thread to process any pending events. This method
* also waits for any {@link SwingUpdateManager}s that have pending events