diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/DeleteAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/DeleteAction.java index fcc0231546..b3e93e7ee8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/DeleteAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/DeleteAction.java @@ -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 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 getNodes(TreePath[] paths) { + List 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 nodes) { + List children = orgNode.getChildren(); + for (GTreeNode node : children) { + if (node instanceof SymbolNode symbolNode) { + nodes.add(symbolNode); + } + else if (node instanceof OrganizationNode childOrgNode) { + getNodes(childOrgNode, nodes); + } + } } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java index ef8f54b214..c796c04fa3 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin2Test.java @@ -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 children = group10.getChildren(); + List 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()); + } } //=================================================================================================