Merge remote-tracking branch

'origin/GP-6944-dragonmacher-symbol-tree-delete-org-node' (Closes #3252)
This commit is contained in:
Ryan Kurtz
2026-06-11 10:13:37 -04:00
2 changed files with 89 additions and 13 deletions
@@ -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,15 +16,18 @@
package ghidra.app.plugin.core.symboltree.actions;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.tree.TreePath;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import ghidra.app.plugin.core.symboltree.SymbolTreeActionContext;
import ghidra.app.plugin.core.symboltree.SymbolTreePlugin;
import ghidra.app.plugin.core.symboltree.nodes.SymbolNode;
import ghidra.app.plugin.core.symboltree.nodes.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
import resources.Icons;
@@ -53,8 +56,11 @@ public class DeleteAction extends SymbolTreeContextAction {
for (TreePath treePath : selectionPaths) {
Object object = treePath.getLastPathComponent();
if (!(object instanceof SymbolNode)) {
return false; // can only delete symbol nodes
if (!(object instanceof SymbolNode) &&
!(object instanceof OrganizationNode)) {
// can only delete symbol nodes or the fake organization nodes, as those are just
// small groups of symbols
return false;
}
}
@@ -64,18 +70,41 @@ public class DeleteAction extends SymbolTreeContextAction {
@Override
public void actionPerformed(SymbolTreeActionContext context) {
TreePath[] selectionPaths = context.getSelectedSymbolTreePaths();
List<SymbolTreeNode> nodes = getNodes(selectionPaths);
Program program = plugin.getProgram();
int transactionID = program.startTransaction("Delete Symbol(s)");
try {
for (TreePath treePath : selectionPaths) {
SymbolNode symbolNode = (SymbolNode) treePath.getLastPathComponent();
Symbol symbol = symbolNode.getSymbol();
program.withTransaction("Delete Symbols(s)", () -> {
for (SymbolTreeNode node : nodes) {
Symbol symbol = node.getSymbol();
symbol.delete();
symbolNode.getParent().removeNode(symbolNode);
node.getParent().removeNode(node);
}
});
}
private List<SymbolTreeNode> getNodes(TreePath[] paths) {
List<SymbolTreeNode> nodes = new ArrayList<>();
for (TreePath treePath : paths) {
Object object = treePath.getLastPathComponent();
if (object instanceof SymbolNode symbolNode) {
nodes.add(symbolNode);
}
else if (object instanceof OrganizationNode orgNode) {
getNodes(orgNode, nodes);
}
}
finally {
program.endTransaction(transactionID, true);
return nodes;
}
private void getNodes(OrganizationNode orgNode, List<SymbolTreeNode> nodes) {
List<GTreeNode> children = orgNode.getChildren();
for (GTreeNode node : children) {
if (node instanceof SymbolNode symbolNode) {
nodes.add(symbolNode);
}
else if (node instanceof OrganizationNode childOrgNode) {
getNodes(childOrgNode, nodes);
}
}
}
}
@@ -18,6 +18,8 @@ package ghidra.app.plugin.core.symboltree;
import static org.junit.Assert.*;
import java.awt.Rectangle;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
@@ -723,7 +725,52 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest {
// verify node is in the tree
assertClassNodes("ClassGroup1::ClassGroup10::ClassGroup100::NewClass");
}
@Test
public void testDeleteOrgNode() throws Exception {
//
// Tests that OrganizationNodes can be deleted from the UI
//
// set org node threshold to a low value
ToolOptions options = tool.getOptions(SymbolTreePlugin.OPTIONS_CATEGORY);
int newThreshold = 4;
options.setInt(SymbolTreePlugin.OPTION_NAME_GROUP_THRESHOLD, newThreshold);
/*
Create enough nodes to trigger an org node.
Namespaces
NsGroup1
NsGroup10
NsGroup100
NsGroup2
NsGroup3
*/
createOrgNamespaces(newThreshold);
openNamespaceNodes("NsGroup1::NsGroup10::NsGroup100");
GTreeNode group10 = getNode(tree, "Global", "Namespaces", "NsGroup1", "NsGroup10");
List<GTreeNode> children = group10.getChildren();
List<SymbolNode> symbols =
children.stream().map(n -> (SymbolNode) n).collect(Collectors.toList());
util.selectNode(group10);
ActionContext context = util.getSymbolTreeContext();
performTreeAction(deleteAction, context);
GTreeNode group1 = getNode(tree, "Global", "Namespaces", "NsGroup1");
group10 = group1.getChild("NsGroup10");
assertNull(group10);
for (SymbolNode node : symbols) {
Symbol symbol = node.getSymbol();
assertTrue(symbol.isDeleted());
}
}
//=================================================================================================