From 6e255143fb6e3a4f7846acd9027f57d7342ecb3e Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Sat, 4 May 2024 10:01:40 -0400 Subject: [PATCH] GP-3849 - Symbol Tree - Added snapshot feature --- Ghidra/Features/Base/certification.manifest | 1 - .../topics/SymbolTreePlugin/SymbolTree.htm | 42 ++- .../app/cmd/refs/SetExternalNameCmd.java | 4 +- .../actions/FindDataTypesBySizeAction.java | 23 +- .../actions/FindStructuresByOffsetAction.java | 27 +- .../actions/SecondaryTreeFilterProvider.java | 51 ++++ .../app/plugin/core/gotoquery/GoToHelper.java | 2 +- .../DisconnectedSymbolTreeProvider.java | 181 ++++++++++++ .../plugin/core/symboltree/SymbolGTree.java | 27 +- .../symboltree/SymbolTreeActionContext.java | 53 ++++ .../core/symboltree/SymbolTreePlugin.java | 64 ++++- .../core/symboltree/SymbolTreeProvider.java | 138 ++++++---- .../actions/CloneSymbolTreeAction.java | 47 ++++ .../core/symboltree/actions/RenameAction.java | 21 +- .../symboltree/actions/SelectionAction.java | 2 +- .../actions/ShowSymbolReferencesAction.java | 2 +- .../actions/SymbolTreeContextAction.java | 2 + .../symboltree/nodes/ClassCategoryNode.java | 2 +- .../nodes/ConfigurableSymbolTreeRootNode.java | 74 +++++ .../symboltree/nodes/ExportsCategoryNode.java | 7 +- .../nodes/FunctionCategoryNode.java | 2 +- .../nodes/NamespaceCategoryNode.java | 2 +- .../symboltree/nodes/SymbolCategoryNode.java | 41 ++- .../core/symboltree/nodes/SymbolTreeNode.java | 4 +- .../symboltree/nodes/SymbolTreeRootNode.java | 66 ++--- .../plugin/core/symtable/SymbolRowObject.java | 4 +- .../core/symtable/SymbolTableModel.java | 13 +- .../docking/widgets/tree/GTreeFilterTest.java | 30 +- .../symboltree/SymbolTreePlugin4Test.java | 258 +++++++++++++++++- .../core/symboltree/SymbolTreeTestUtils.java | 57 +++- .../core/functiongraph/FGActionManager.java | 2 +- .../widgets/filter/FilterTextField.java | 37 ++- .../tree/DefaultGTreeFilterProvider.java | 49 +++- .../main/java/docking/widgets/tree/GTree.java | 25 +- .../widgets/tree/GTreeFilterProvider.java | 20 +- .../java/docking/widgets/tree/GTreeNode.java | 1 - .../widgets/tree/support/GTreeRenderer.java | 6 +- .../tree/tasks/GTreeExpandPathsTask.java | 9 +- 38 files changed, 1140 insertions(+), 256 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/SecondaryTreeFilterProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/DisconnectedSymbolTreeProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/CloneSymbolTreeAction.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ConfigurableSymbolTreeRootNode.java diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 51ca6cb420..8238c745f2 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -775,7 +775,6 @@ src/main/resources/images/eclipse.png||GHIDRA||||END| src/main/resources/images/edit-bomb.png||Oxygen Icons - LGPL 3.0||||END| src/main/resources/images/editbytes.gif||GHIDRA||||END| src/main/resources/images/emblem-favorite.png||Tango Icons - Public Domain|||tango|END| -src/main/resources/images/empty8x16.png||GHIDRA||||END| src/main/resources/images/emptyFragment.gif||GHIDRA||||END| src/main/resources/images/emptyFragmentInView.gif||GHIDRA||||END| src/main/resources/images/enum.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/SymbolTreePlugin/SymbolTree.htm b/Ghidra/Features/Base/src/main/help/help/topics/SymbolTreePlugin/SymbolTree.htm index 479c4d1115..749b0e0473 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/SymbolTreePlugin/SymbolTree.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/SymbolTreePlugin/SymbolTree.htm @@ -348,7 +348,7 @@

View Qualified Names in Code Browser

- +

To include namespace names in the display of labels and names within the Code Browser, select Edit Tool Options... from the @@ -359,6 +359,46 @@ "help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm">CodeBrowser Options).

+ + + +

Symbol Tree Snapshots

+ +
+

Pressing the action will create a copy of the current + Symbol Tree in a new window. The new disconnected Symbol Tree will not respond to program + activation events. This allows users to keep the new window open while working with various + programs, without affecting the contents. +

+ +

Symbol Tree Clone Action

+ +
+

Creates a new disconnected snapshot (cloned) view of the Symbol Tree. This action is on + the primary Symbol Tree, as well as any cloned symbol trees.

+
+ +

Disable Category

+ +
+

When working in a snapshot Symbol Tree, you can choose to disable a root-level folder + by right-clicking and selected Disable Category. Once disabled, the node will remain + in the tree with a disabled icon. A disabled node will no longer show any children. +

+
+ +

Enable Category

+ +
+

This action is used to re-enable categories that were previously disabled. +

+
+ +
+ + + +

Provided By: SymbolTreePlugin

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/refs/SetExternalNameCmd.java b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/refs/SetExternalNameCmd.java index 8f7b8f3915..43055175c3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/refs/SetExternalNameCmd.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/refs/SetExternalNameCmd.java @@ -21,8 +21,6 @@ import ghidra.util.exception.InvalidInputException; /** * Command for setting the external program name and path. - * - * */ public class SetExternalNameCmd implements Command { @@ -34,7 +32,7 @@ public class SetExternalNameCmd implements Command { /** * Constructs a new command for setting the external program name and path. * @param externalName the name of the link. - * @param externalPath the path of the file to assocate with this link. + * @param externalPath the path of the file to associate with this link. */ public SetExternalNameCmd(String externalName, String externalPath) { this.externalName = externalName; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindDataTypesBySizeAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindDataTypesBySizeAction.java index 699b2c99aa..43fb93b079 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindDataTypesBySizeAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindDataTypesBySizeAction.java @@ -19,8 +19,7 @@ import docking.ActionContext; import docking.action.DockingAction; import docking.action.MenuData; import docking.widgets.dialogs.NumberRangeInputDialog; -import docking.widgets.tree.*; -import docking.widgets.tree.support.CombinedGTreeFilter; +import docking.widgets.tree.GTreeNode; import docking.widgets.tree.support.GTreeFilter; import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; import ghidra.app.plugin.core.datamgr.DataTypesProvider; @@ -64,7 +63,7 @@ public class FindDataTypesBySizeAction extends DockingAction { newProvider.setTitle(getName()); DataTypeArchiveGTree tree = newProvider.getGTree(); GTreeFilter filter = createFilter(values); - tree.setFilterProvider(new MyTreeFilterProvider(tree, filter)); + tree.setFilterProvider(new SecondaryTreeFilterProvider(tree, filter)); newProvider.setVisible(true); } @@ -72,24 +71,6 @@ public class FindDataTypesBySizeAction extends DockingAction { return new SizeGTreeFilter(values); } - private class MyTreeFilterProvider extends DefaultGTreeFilterProvider { - private GTreeFilter secondaryFilter; - - MyTreeFilterProvider(GTree tree, GTreeFilter secondaryFilter) { - super(tree); - this.secondaryFilter = secondaryFilter; - } - - @Override - public GTreeFilter getFilter() { - GTreeFilter filter = super.getFilter(); - if (filter == null) { - return secondaryFilter; - } - return new CombinedGTreeFilter(filter, secondaryFilter); - } - } - private class SizeGTreeFilter implements GTreeFilter { private final SortedRangeList sizes; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindStructuresByOffsetAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindStructuresByOffsetAction.java index f3763bdf57..18181e2efd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindStructuresByOffsetAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/FindStructuresByOffsetAction.java @@ -21,8 +21,7 @@ import docking.ActionContext; import docking.action.DockingAction; import docking.action.MenuData; import docking.widgets.dialogs.NumberRangeInputDialog; -import docking.widgets.tree.*; -import docking.widgets.tree.support.CombinedGTreeFilter; +import docking.widgets.tree.GTreeNode; import docking.widgets.tree.support.GTreeFilter; import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; import ghidra.app.plugin.core.datamgr.DataTypesProvider; @@ -56,8 +55,7 @@ public class FindStructuresByOffsetAction extends DockingAction { @Override public void actionPerformed(ActionContext context) { - NumberRangeInputDialog inputDialog = - new NumberRangeInputDialog(NAME, "Offset(s)"); + NumberRangeInputDialog inputDialog = new NumberRangeInputDialog(NAME, "Offset(s)"); if (!inputDialog.show()) { return; } @@ -66,28 +64,11 @@ public class FindStructuresByOffsetAction extends DockingAction { DataTypesProvider newProvider = plugin.createProvider(); newProvider.setTitle(NAME); DataTypeArchiveGTree tree = newProvider.getGTree(); - tree.setFilterProvider(new MyTreeFilterProvider(tree, new OffsetGTreeFilter(values))); + tree.setFilterProvider( + new SecondaryTreeFilterProvider(tree, new OffsetGTreeFilter(values))); newProvider.setVisible(true); } - private class MyTreeFilterProvider extends DefaultGTreeFilterProvider { - private GTreeFilter secondaryFilter; - - MyTreeFilterProvider(GTree tree, GTreeFilter secondaryFilter) { - super(tree); - this.secondaryFilter = secondaryFilter; - } - - @Override - public GTreeFilter getFilter() { - GTreeFilter filter = super.getFilter(); - if (filter == null) { - return secondaryFilter; - } - return new CombinedGTreeFilter(filter, secondaryFilter); - } - } - private class OffsetGTreeFilter implements GTreeFilter { private final SortedRangeList offsets; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/SecondaryTreeFilterProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/SecondaryTreeFilterProvider.java new file mode 100644 index 0000000000..43969e8f74 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/SecondaryTreeFilterProvider.java @@ -0,0 +1,51 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.datamgr.actions; + +import docking.widgets.tree.*; +import docking.widgets.tree.support.CombinedGTreeFilter; +import docking.widgets.tree.support.GTreeFilter; + +/** + * A filter that allows for an additional second filter. + */ +public class SecondaryTreeFilterProvider extends DefaultGTreeFilterProvider { + + private GTreeFilter secondaryFilter; + + SecondaryTreeFilterProvider(GTree tree, GTreeFilter secondaryFilter) { + super(tree); + this.secondaryFilter = secondaryFilter; + } + + @Override + public GTreeFilter getFilter() { + GTreeFilter filter = super.getFilter(); + if (filter == null) { + return secondaryFilter; + } + return new CombinedGTreeFilter(filter, secondaryFilter); + } + + @Override + public GTreeFilterProvider copy(GTree newTree) { + // For now, we shouldn't need to copy the secondary filter. It's current uses are to not + // change the filter once it has been created. + SecondaryTreeFilterProvider newProvider = + new SecondaryTreeFilterProvider(newTree, secondaryFilter); + return newProvider; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java index e5a54beb51..b5f6d074ff 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java @@ -449,7 +449,7 @@ public class GoToHelper { ExternalManager externalManager = program.getExternalManager(); String externalLibraryPath = externalManager.getExternalLibraryPath(extProgName); if (!pathName.equals(externalLibraryPath)) { - Command cmd = new SetExternalNameCmd(extProgName, domainFile.getPathname()); + Command cmd = new SetExternalNameCmd(extProgName, domainFile.getPathname()); tool.execute(cmd, program); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/DisconnectedSymbolTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/DisconnectedSymbolTreeProvider.java new file mode 100644 index 0000000000..41e99f893e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/DisconnectedSymbolTreeProvider.java @@ -0,0 +1,181 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.symboltree; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import docking.WindowPosition; +import docking.action.KeyBindingData; +import docking.action.builder.ActionBuilder; +import ghidra.app.nav.DecoratorPanel; +import ghidra.app.plugin.core.symboltree.nodes.*; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.util.HelpLocation; + +/** + * A disconnected symbol tree is a snapshot of the primary symbol tree. + */ +public class DisconnectedSymbolTreeProvider extends SymbolTreeProvider { + + private static final String WINDOW_GROUP = "Disconnected Symbol Tree"; + + public DisconnectedSymbolTreeProvider(PluginTool tool, SymbolTreePlugin plugin, + Program program) { + super(tool, plugin); + + setDefaultWindowPosition(WindowPosition.WINDOW); + + createActions(); + + // Snapshots do not usually track events. Turn this off now, but leave the action so + // clients can turn the action on as desired. + goToToggleAction.setEnabled(false); + + setHelpLocation(new HelpLocation("SymbolTreePlugin", "Disconnected_Symbol_Tree")); + + this.program = program; + program.addListener(domainObjectListener); + + rebuildTree(); + } + + @Override + public String getWindowGroup() { + return WINDOW_GROUP; + } + + @Override + public WindowPosition getDefaultWindowPosition() { + return WindowPosition.WINDOW; + } + + @Override + public boolean isTransient() { + return true; + } + + @Override + public boolean isSnapshot() { + return true; + } + + @Override + protected void addToToolbar() { + // do not add the disconnected provider to the toolbar + } + + @Override + protected void setKeyBinding(KeyBindingData kbData) { + // no keybinding for the disconnected provider + } + + @Override + void setProgram(Program newProgram) { + // nothing to do; we maintain our state as the user changes programs + } + + @Override + void programDeactivated(Program deactivatedProgram) { + // nothing to do; we maintain our state as the user changes programs + } + + @Override + void programClosed(Program closedProgram) { + tree.cancelWork(); + + closedProgram.removeListener(domainObjectListener); + + program = null; + rebuildTree(); + + closeComponent(); + } + + @Override + protected JPanel createMainPanel(JComponent contentComponent) { + return new DecoratorPanel(contentComponent, false); + } + + @Override + protected SymbolTreeRootNode createRootNode() { + return new ConfigurableSymbolTreeRootNode(program); + } + + @Override + public void closeComponent() { + plugin.closeDisconnectedProvider(this); + } + + @Override + protected void transferSettings(DisconnectedSymbolTreeProvider newProvider) { + + // transfer disabled node settings + ConfigurableSymbolTreeRootNode myModelRoot = + (ConfigurableSymbolTreeRootNode) tree.getModelRoot(); + + ConfigurableSymbolTreeRootNode newModelRoot = + (ConfigurableSymbolTreeRootNode) newProvider.tree.getModelRoot(); + myModelRoot.transferSettings(newModelRoot); + + super.transferSettings(newProvider); + } + + @Override + void writeConfigState(SaveState saveState) { + // we have no state we are interested in saving + } + + @Override + void readConfigState(SaveState saveState) { + // we have no state we are interested in loading + } + + private void createActions() { + + //@formatter:off + new ActionBuilder("Enable Category", plugin.getName()) + .popupMenuPath("Enable Category") + .withContext(SymbolTreeActionContext.class) + .enabledWhen(c -> { + SymbolTreeNode node = c.getSelectedNode(); + return node instanceof SymbolCategoryNode; + }) + .onAction(c -> { + SymbolCategoryNode node = (SymbolCategoryNode) c.getSelectedNode(); + node.setEnabled(true); + }) + .buildAndInstallLocal(this); + //@formatter:on + + //@formatter:off + new ActionBuilder("Disable Category", plugin.getName()) + .popupMenuPath("Disable Category") + .withContext(SymbolTreeActionContext.class) + .enabledWhen(c -> { + SymbolTreeNode node = c.getSelectedNode(); + return node instanceof SymbolCategoryNode; + }) + .onAction(c -> { + SymbolCategoryNode node = (SymbolCategoryNode) c.getSelectedNode(); + node.setEnabled(false); + }) + .buildAndInstallLocal(this); + //@formatter:on + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolGTree.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolGTree.java index b23a94bcb1..6ae4991678 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolGTree.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolGTree.java @@ -21,14 +21,15 @@ import java.awt.Component; import javax.swing.*; import javax.swing.tree.TreePath; -import docking.widgets.tree.GTree; -import docking.widgets.tree.GTreeNode; +import docking.widgets.tree.*; import docking.widgets.tree.support.GTreeRenderer; import generic.theme.GIcon; +import ghidra.app.plugin.core.symboltree.nodes.SymbolCategoryNode; import ghidra.app.plugin.core.symboltree.nodes.SymbolNode; import ghidra.app.util.SymbolInspector; import ghidra.program.model.listing.Program; import ghidra.program.model.symbol.Symbol; +import resources.ResourceManager; public class SymbolGTree extends GTree { @@ -44,6 +45,14 @@ public class SymbolGTree extends GTree { setDragNDropHandler(new SymbolGTreeDragNDropHandler(plugin)); setAccessibleNamePrefix("Symbol"); + + setRootNodeAllowedToCollapse(false); + } + + // open access + @Override + protected void setFilterRestoreState(GTreeState state) { + super.setFilterRestoreState(state); } @Override @@ -95,6 +104,20 @@ public class SymbolGTree extends GTree { return label; } + + @Override + protected Icon getNodeIcon(GTreeNode node, boolean expanded) { + + Icon icon = super.getNodeIcon(node, expanded); + + if (node instanceof SymbolCategoryNode symbolNode) { + if (!symbolNode.isEnabled()) { + return ResourceManager.getDisabledIcon(icon); + } + } + + return icon; + } } public void setProgram(Program program) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeActionContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeActionContext.java index ed009ebac2..ad004fae9e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeActionContext.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeActionContext.java @@ -22,6 +22,7 @@ import javax.swing.tree.TreePath; import ghidra.app.context.ProgramSymbolActionContext; import ghidra.app.plugin.core.symboltree.nodes.SymbolNode; +import ghidra.app.plugin.core.symboltree.nodes.SymbolTreeNode; import ghidra.program.model.listing.Program; import ghidra.program.model.symbol.Symbol; @@ -54,6 +55,58 @@ public class SymbolTreeActionContext extends ProgramSymbolActionContext { return null; } + /** + * Returns a symbol tree node if there is a single node selected and it is a symbol tree node. + * Otherwise, null is returned. + * @return the selected node or null + */ + public SymbolTreeNode getSelectedNode() { + if (selectionPaths != null && selectionPaths.length == 1) { + Object object = selectionPaths[0].getLastPathComponent(); + if (object instanceof SymbolTreeNode node) { + return node; + } + } + return null; + } + + /** + * Returns true if the tree's current selection contains at least one {@link SymbolNode}. + * @return true if the tree's current selection contains at least one {@link SymbolNode}. + */ + public boolean hasSymbolsSelected() { + if (selectionPaths == null) { + return false; + } + + for (TreePath treePath : selectionPaths) { + Object object = treePath.getLastPathComponent(); + if (object instanceof SymbolNode) { + return true; + } + } + return false; + } + + /** + * Returns all selected {@link SymbolNode}s or an empty list. + * @return all selected {@link SymbolNode}s or an empty list. + */ + public List getSelectedSymbolNodes() { + if (selectionPaths == null) { + return List.of(); + } + + List symbols = new ArrayList<>(); + for (TreePath treePath : selectionPaths) { + Object object = treePath.getLastPathComponent(); + if (object instanceof SymbolNode) { + symbols.add((SymbolNode) object); + } + } + return symbols; + } + private static List getSymbols(TreePath[] selectionPaths) { if (selectionPaths == null) { return null; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java index 52cb02c549..a55242bbbe 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java @@ -15,6 +15,9 @@ */ package ghidra.app.plugin.core.symboltree; +import java.util.ArrayList; +import java.util.List; + import ghidra.app.CorePluginPackage; import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; @@ -43,14 +46,15 @@ public class SymbolTreePlugin extends Plugin { public static final String PLUGIN_NAME = "SymbolTreePlugin"; - private SymbolTreeProvider provider; + private SymbolTreeProvider connectedProvider; + private List disconnectedProviders = new ArrayList<>(); private Program program; private GoToService goToService; private boolean processingGoTo; public SymbolTreePlugin(PluginTool tool) { super(tool); - provider = new SymbolTreeProvider(tool, this); + connectedProvider = new SymbolTreeProvider(tool, this); } @Override @@ -60,14 +64,13 @@ public class SymbolTreePlugin extends Plugin { Program oldProgram = program; program = ev.getActiveProgram(); if (oldProgram != null) { - provider.programDeactivated(oldProgram); - } - if (program != null) { - provider.programActivated(program); + connectedProvider.programDeactivated(oldProgram); } + + connectedProvider.setProgram(program); } else if (event instanceof ProgramClosedPluginEvent) { - provider.programClosed(((ProgramClosedPluginEvent) event).getProgram()); + programClosed(((ProgramClosedPluginEvent) event).getProgram()); } else if (event instanceof ProgramLocationPluginEvent) { if (processingGoTo) { @@ -75,10 +78,32 @@ public class SymbolTreePlugin extends Plugin { } ProgramLocation loc = ((ProgramLocationPluginEvent) event).getLocation(); - provider.locationChanged(loc); + connectedProvider.locationChanged(loc); + + for (SymbolTreeProvider provider : disconnectedProviders) { + provider.locationChanged(loc); + } } } + private void programClosed(Program p) { + + connectedProvider.programClosed(p); + + List copy = new ArrayList<>(disconnectedProviders); + for (SymbolTreeProvider provider : copy) { + if (provider.getProgram() == p) { + closeDisconnectedProvider(provider); + } + } + } + + void closeDisconnectedProvider(SymbolTreeProvider provider) { + disconnectedProviders.remove(provider); + tool.removeComponentProvider(provider); + provider.dispose(); + } + @Override protected void init() { goToService = tool.getService(GoToService.class); @@ -86,19 +111,24 @@ public class SymbolTreePlugin extends Plugin { @Override protected void dispose() { - tool.removeComponentProvider(provider); - provider.dispose(); + tool.removeComponentProvider(connectedProvider); + connectedProvider.dispose(); program = null; + + List copy = new ArrayList<>(disconnectedProviders); + for (SymbolTreeProvider provider : copy) { + closeDisconnectedProvider(provider); + } } @Override public void readConfigState(SaveState saveState) { - provider.readConfigState(saveState); + connectedProvider.readConfigState(saveState); } @Override public void writeConfigState(SaveState saveState) { - provider.writeConfigState(saveState); + connectedProvider.writeConfigState(saveState); } public void goTo(Symbol symbol) { @@ -145,6 +175,14 @@ public class SymbolTreePlugin extends Plugin { } SymbolTreeProvider getProvider() { - return provider; + return connectedProvider; + } + + public DisconnectedSymbolTreeProvider createNewDisconnectedProvider(Program p) { + DisconnectedSymbolTreeProvider newProvider = + new DisconnectedSymbolTreeProvider(tool, this, p); + disconnectedProviders.add(newProvider); + tool.showComponentProvider(newProvider, true); + return newProvider; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java index a4a9b530f5..2175ed71d2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java @@ -60,15 +60,15 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { private ClipboardOwner clipboardOwner; private Clipboard localClipboard;// temporary clipboard used for the "cut" operation - private DomainObjectListener domainObjectListener; - private Program program; + protected DomainObjectListener domainObjectListener; + protected Program program; - private final SymbolTreePlugin plugin; - private SymbolGTree tree; - private JPanel mainPanel; - private JComponent component; + protected SymbolTreePlugin plugin; + protected SymbolGTree tree; + protected JPanel mainPanel; + protected JComponent component; - private GoToToggleAction goToToggleAction; + protected GoToToggleAction goToToggleAction; /** * A list into which tasks to be run will accumulated until we put them into the GTree's @@ -108,6 +108,8 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { super(tool, NAME, plugin.getName()); this.plugin = plugin; + setWindowMenuGroup(NAME); + setIcon(ICON); addToToolbar(); @@ -127,16 +129,27 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { // Setup Methods //================================================================================================== - private JComponent buildProvider() { - mainPanel = new JPanel(new BorderLayout()); + protected JPanel createMainPanel(JComponent contentComponent) { + JPanel panel = new JPanel(new BorderLayout()); - tree = createTree(new SymbolTreeRootNode()); - mainPanel.add(tree, BorderLayout.CENTER); + panel.add(contentComponent, BorderLayout.CENTER); + + return panel; + } + + protected SymbolTreeRootNode createRootNode() { + return new SymbolTreeRootNode(program); + } + + private JComponent buildProvider() { + + tree = createTree(createRootNode()); // There's no reason to see the root node in this window. The name (GLOBAL) is // unimportant and the tree is never collapsed at this level. tree.setRootVisible(false); + mainPanel = createMainPanel(tree); return mainPanel; } @@ -219,8 +232,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { SymbolNode node = (SymbolNode) object; Symbol symbol = node.getSymbol(); SymbolType type = symbol.getSymbolType(); - if (!type.isNamespace() || - type == SymbolType.FUNCTION) { + if (!type.isNamespace() || type == SymbolType.FUNCTION) { plugin.goTo(symbol); } } @@ -247,8 +259,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { deleteAction.setEnabled(false); DockingAction referencesAction = - new ShowSymbolReferencesAction(plugin.getTool(), - plugin.getName()); + new ShowSymbolReferencesAction(plugin.getTool(), plugin.getName()); DockingAction selectionAction = new SelectionAction(plugin); selectionAction.setEnabled(false); @@ -257,6 +268,8 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { DockingAction goToExternalAction = new GoToExternalLocationAction(plugin); goToExternalAction.setEnabled(false); + CloneSymbolTreeAction cloneAction = new CloneSymbolTreeAction(plugin, this); + tool.addLocalAction(this, createImportAction); tool.addLocalAction(this, setExternalProgramAction); tool.addLocalAction(this, createExternalLocationAction); @@ -272,6 +285,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { tool.addLocalAction(this, goToToggleAction); tool.addLocalAction(this, selectionAction); tool.addLocalAction(this, goToExternalAction); + tool.addLocalAction(this, cloneAction); } //================================================================================================== @@ -300,24 +314,56 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { // Class Methods //================================================================================================== - void programActivated(Program openedProgram) { - this.program = openedProgram; - if (tool.isVisible(this)) { - setProgram(openedProgram); - } + GTree getTree() { + return tree; } - private void setProgram(Program program) { + public void cloneWindow() { + + DisconnectedSymbolTreeProvider newProvider = plugin.createNewDisconnectedProvider(program); + + Swing.runLater(() -> { + newProvider.setProgram(program); + transferSettings(newProvider); + }); + } + + /** + * Called to have this symbol tree provider copy settings into the given provider. + * @param newProvider the new provider + */ + protected void transferSettings(DisconnectedSymbolTreeProvider newProvider) { + // + // Unusual Code: We want to copy the current tree state to the new tree. Since we are + // also applying the filter state below, the tree will use the 'filter restore state' + // after the filter has been applied. Thus, we need to set the filter restore state + // instead of using the GTree's restoreTreeState() method. + // + GTreeState treeState = tree.getTreeState(); + newProvider.tree.setFilterRestoreState(treeState); + + GTreeFilterProvider filterProvider = tree.getFilterProvider(); + GTreeFilterProvider newFilterProvider = filterProvider.copy(newProvider.tree); + newProvider.tree.setFilterProvider(newFilterProvider); + } + + public Program getProgram() { + return program; + } + + void setProgram(Program program) { + this.program = program; + if (!isVisible()) { + return; + } + if (program == null) { return; } program.addListener(domainObjectListener); - mainPanel.remove(tree); - tree = createTree(new SymbolTreeRootNode(program)); - mainPanel.add(tree); - component.validate(); + rebuildTree(); // restore any state that may be saved GTreeState treeState = treeStateMap.get(program); @@ -335,11 +381,15 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { GTreeState treeState = tree.getTreeState(); treeStateMap.put(program, treeState); + rebuildTree(); + this.program = null; + } + + protected void rebuildTree() { mainPanel.remove(tree); - tree = createTree(new SymbolTreeRootNode()); + tree = createTree(createRootNode()); mainPanel.add(tree); component.validate(); - this.program = null; } void programClosed(Program closedProgram) { @@ -371,14 +421,11 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { } catch (DuplicateNameException e) { sb.append("Parent namespace " + namespace.getName() + - " contains namespace named " + symbol.getName() + - "\n"); + " contains namespace named " + symbol.getName() + "\n"); } catch (InvalidInputException | CircularDependencyException e) { - sb.append("Could not change parent namespace for " + symbol.getName() + - ": " + - e.getMessage() + - "\n"); + sb.append("Could not change parent namespace for " + symbol.getName() + ": " + + e.getMessage() + "\n"); } } } @@ -405,8 +452,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { return true; } // the symbol to move does not allow dups, so make sure all existing symbols do allow dups. - List symbols = symbolTable.getSymbols(symbol.getName(), - destinationNamespace); + List symbols = symbolTable.getSymbols(symbol.getName(), destinationNamespace); for (Symbol s : symbols) { if (!s.getSymbolType().allowsDuplicates()) { return false; @@ -421,7 +467,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { (symbolType == SymbolType.NAMESPACE) || (symbolType == SymbolType.CLASS); } - private void rebuildTree() { + private void reloadTree() { // If we do not cancel the edit here, then an open edits will instead be committed. It // seems safer to cancel an edit rather than to commit it without asking. @@ -458,18 +504,15 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { domainChangeUpdateManager.update(); } - void showComponent(Program currentProgram) { - if (!tool.isVisible(this)) { - setProgram(currentProgram); - } - tool.showComponentProvider(this, true); - } - public void locationChanged(ProgramLocation loc) { if (!goToToggleAction.isSelected()) { return; } + if (program != loc.getProgram()) { + return; + } + Symbol symbol = null; Address addr = loc.getAddress(); if (loc instanceof VariableLocation) { @@ -547,7 +590,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { // @formatter:off return new DomainObjectListenerBuilder(this) .ignoreWhen(this::ignoreEvents) - .any(RESTORED).terminate(this::rebuildTree) + .any(RESTORED).terminate(this::reloadTree) .with(ProgramChangeRecord.class) .each(SYMBOL_RENAMED).call(this::processSymbolRenamed) .each(SYMBOL_DATA_CHANGED, SYMBOL_SCOPE_CHANGED).call(this::processSymbolChanged) @@ -665,8 +708,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { @Override public String toString() { - return getClass().getSimpleName() + - " " + symbol; + return getClass().getSimpleName() + " " + symbol; } } @@ -721,7 +763,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { @Override void doRun(TaskMonitor monitor) throws CancelledException { SymbolTreeRootNode root = (SymbolTreeRootNode) tree.getModelRoot(); - root.symbolRemoved(symbol, monitor); + root.symbolRemoved(symbol, symbol.getName(), monitor); tree.refilterLater(); } } @@ -743,7 +785,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { public void runBulk(TaskMonitor monitor) throws CancelledException { if (tasks.size() > MAX_TASK_COUNT) { - Swing.runLater(() -> rebuildTree()); + Swing.runLater(() -> reloadTree()); return; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/CloneSymbolTreeAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/CloneSymbolTreeAction.java new file mode 100644 index 0000000000..e87fa3ed5a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/CloneSymbolTreeAction.java @@ -0,0 +1,47 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.symboltree.actions; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.ToolBarData; +import generic.theme.GIcon; +import ghidra.app.plugin.core.symboltree.SymbolTreePlugin; +import ghidra.app.plugin.core.symboltree.SymbolTreeProvider; + +public class CloneSymbolTreeAction extends DockingAction { + + private SymbolTreeProvider provider; + + public CloneSymbolTreeAction(SymbolTreePlugin plugin, SymbolTreeProvider provider) { + super("Symbol Tree Clone", plugin.getName()); + this.provider = provider; + + setToolBarData(new ToolBarData(new GIcon("icon.provider.clone"))); + setDescription("Create a snapshot (disconnected) copy of this Symbol Tree window"); + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + return provider.getProgram() != null; + } + + @Override + public void actionPerformed(ActionContext context) { + provider.cloneWindow(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/RenameAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/RenameAction.java index fd142c52a3..21adb37ee1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/RenameAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/RenameAction.java @@ -15,37 +15,28 @@ */ package ghidra.app.plugin.core.symboltree.actions; -import javax.swing.tree.TreePath; - 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.SymbolTreeNode; public class RenameAction extends SymbolTreeContextAction { public RenameAction(SymbolTreePlugin plugin) { super("Rename Symbol", plugin.getName()); - setPopupMenuData(new MenuData(new String[] { "Rename" }, null, "xxx", MenuData.NO_MNEMONIC, - "1")); + setPopupMenuData( + new MenuData(new String[] { "Rename" }, null, "xxx", MenuData.NO_MNEMONIC, "1")); } @Override public boolean isEnabledForContext(SymbolTreeActionContext context) { - TreePath[] selectionPaths = context.getSelectedSymbolTreePaths(); - if (selectionPaths.length == 1) { - Object object = selectionPaths[0].getLastPathComponent(); - return (object instanceof SymbolNode); - } - return false; + SymbolTreeNode node = context.getSelectedNode(); + return node != null; } @Override public void actionPerformed(SymbolTreeActionContext context) { - TreePath[] selectionPaths = context.getSelectedSymbolTreePaths(); - GTreeNode node = (GTreeNode) selectionPaths[0].getLastPathComponent(); - context.getSymbolTree().startEditing(node); + context.getSymbolTree().startEditing(context.getSelectedNode()); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SelectionAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SelectionAction.java index 15a031df59..2f6a7a8f43 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SelectionAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SelectionAction.java @@ -34,7 +34,7 @@ public class SelectionAction extends SymbolTreeContextAction { public SelectionAction(Plugin plugin) { super("Make Selection", plugin.getName()); this.plugin = plugin; - setPopupMenuData(new MenuData(new String[] { "Make Selection" }, "0Middle")); + setPopupMenuData(new MenuData(new String[] { "Make Selection" }, MIDDLE_MENU_GROUP)); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/ShowSymbolReferencesAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/ShowSymbolReferencesAction.java index 8154b12910..09aa3dad0c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/ShowSymbolReferencesAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/ShowSymbolReferencesAction.java @@ -59,7 +59,7 @@ public class ShowSymbolReferencesAction extends SymbolTreeContextAction { super(AbstractFindReferencesDataTypeAction.NAME, owner, KeyBindingType.SHARED); this.tool = tool; - setPopupMenuData(new MenuData(new String[] { "Show References to" }, "0Middle")); + setPopupMenuData(new MenuData(new String[] { "Show References to" }, MIDDLE_MENU_GROUP)); installHelpLocation(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SymbolTreeContextAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SymbolTreeContextAction.java index 4cd532a4c4..0c21d71047 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SymbolTreeContextAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SymbolTreeContextAction.java @@ -24,6 +24,8 @@ import ghidra.app.plugin.core.symboltree.SymbolTreeActionContext; public abstract class SymbolTreeContextAction extends DockingAction { + protected static final String MIDDLE_MENU_GROUP = "0Middle"; + public SymbolTreeContextAction(String name, String owner) { super(name, owner); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ClassCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ClassCategoryNode.java index 2803fa7b88..708b50d3e0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ClassCategoryNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ClassCategoryNode.java @@ -36,7 +36,7 @@ public class ClassCategoryNode extends SymbolCategoryNode { public static final Icon CLOSED_FOLDER_CLASSES_ICON = new GIcon("icon.plugin.symboltree.node.category.classes.closed"); - ClassCategoryNode(Program program) { + public ClassCategoryNode(Program program) { super(SymbolCategory.CLASS_CATEGORY, program); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ConfigurableSymbolTreeRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ConfigurableSymbolTreeRootNode.java new file mode 100644 index 0000000000..529c253e8c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ConfigurableSymbolTreeRootNode.java @@ -0,0 +1,74 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.symboltree.nodes; + +import java.util.List; + +import docking.widgets.tree.GTree; +import docking.widgets.tree.GTreeNode; +import ghidra.app.plugin.core.symboltree.DisconnectedSymbolTreeProvider; +import ghidra.program.model.listing.Program; + +/** + * A version of the Symbol Tree's root node that allows users to disable categories. The categories + * themselves track their enabled state. This class supports the cloning of a + * {@link DisconnectedSymbolTreeProvider} by copying the categories' enable state. + */ +public class ConfigurableSymbolTreeRootNode extends SymbolTreeRootNode { + + public ConfigurableSymbolTreeRootNode(Program program) { + super(program); + } + + public void transferSettings(ConfigurableSymbolTreeRootNode otherRoot) { + + if (!isLoaded()) { + return; + } + + List myChildren = getChildren(); + List otherChildren = otherRoot.getChildren(); + for (GTreeNode node : myChildren) { + SymbolCategoryNode myCategoryNode = getModelNode((SymbolCategoryNode) node); + SymbolCategoryNode otherCategoryNode = getMatchingNode(otherChildren, myCategoryNode); + otherCategoryNode.setEnabled(myCategoryNode.isEnabled()); + } + } + + private SymbolCategoryNode getMatchingNode(List nodes, + SymbolCategoryNode nodeToMatch) { + + for (GTreeNode node : nodes) { + if (nodeToMatch.equals(node)) { + return getModelNode((SymbolCategoryNode) node); + } + } + + return null; + } + + private SymbolCategoryNode getModelNode(SymbolCategoryNode node) { + GTree gTree = node.getTree(); + if (gTree != null) { + SymbolCategoryNode modelNode = (SymbolCategoryNode) gTree.getModelNode(node); + if (node != modelNode) { + return modelNode; + } + } + return node; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ExportsCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ExportsCategoryNode.java index 656d64d0cf..31eaaa34ce 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ExportsCategoryNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/ExportsCategoryNode.java @@ -33,14 +33,17 @@ class ExportsCategoryNode extends SymbolCategoryNode { private static final Icon CLOSED_FOLDER = new GIcon("icon.plugin.symboltree.node.category.exports.closed"); - ExportsCategoryNode(Program program) { + public ExportsCategoryNode(Program program) { super(SymbolCategory.EXPORTS_CATEGORY, program); } @Override public List generateChildren(TaskMonitor monitor) { - List list = new ArrayList<>(); + if (!isEnabled) { + return Collections.emptyList(); + } + List list = new ArrayList<>(); List functionSymbolList = getExportSymbols(); for (Symbol symbol : functionSymbolList) { list.add(SymbolNode.createNode(symbol, program)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/FunctionCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/FunctionCategoryNode.java index 336dba25c6..e013a176ac 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/FunctionCategoryNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/FunctionCategoryNode.java @@ -36,7 +36,7 @@ class FunctionCategoryNode extends SymbolCategoryNode { public static final Icon CLOSED_FOLDER_FUNCTIONS_ICON = new GIcon("icon.plugin.symboltree.node.category.function.closed"); - FunctionCategoryNode(Program program) { + public FunctionCategoryNode(Program program) { super(SymbolCategory.FUNCTION_CATEGORY, program); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/NamespaceCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/NamespaceCategoryNode.java index 3a6259efd5..f0527dbdb8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/NamespaceCategoryNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/NamespaceCategoryNode.java @@ -32,7 +32,7 @@ public class NamespaceCategoryNode extends SymbolCategoryNode { public static final Icon CLOSED_FOLDER_NAMESPACES_ICON = new GIcon("icon.plugin.symboltree.node.category.namespace.closed"); - NamespaceCategoryNode(Program program) { + public NamespaceCategoryNode(Program program) { super(SymbolCategory.NAMESPACE_CATEGORY, program); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java index d44db795c5..62b63affdd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolCategoryNode.java @@ -37,23 +37,42 @@ public abstract class SymbolCategoryNode extends SymbolTreeNode { protected GlobalNamespace globalNamespace; protected Program program; - // dummy constructor for no program - protected SymbolCategoryNode() { - symbolCategory = null; - symbolTable = null; - globalNamespace = null; - program = null; + protected boolean isEnabled = true; + + public SymbolCategoryNode(SymbolCategory symbolCategory, Program p) { + this.symbolCategory = symbolCategory; + this.program = p; + this.symbolTable = p == null ? null : p.getSymbolTable(); + this.globalNamespace = p == null ? null : (GlobalNamespace) p.getGlobalNamespace(); } - public SymbolCategoryNode(SymbolCategory symbolCategory, Program program) { - this.symbolCategory = symbolCategory; - this.program = program; - this.symbolTable = program.getSymbolTable(); - this.globalNamespace = (GlobalNamespace) program.getGlobalNamespace(); + public void setEnabled(boolean enabled) { + if (isEnabled == enabled) { + return; + } + + isEnabled = enabled; + unloadChildren(); + + GTree gTree = getTree(); + if (gTree != null) { + SymbolCategoryNode modelNode = (SymbolCategoryNode) gTree.getModelNode(this); + if (this != modelNode) { + modelNode.setEnabled(enabled); + } + } + } + + public boolean isEnabled() { + return isEnabled; } @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { + if (!isEnabled) { + return Collections.emptyList(); + } + SymbolType symbolType = symbolCategory.getSymbolType(); List list = getSymbols(symbolType, monitor); monitor.checkCancelled(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java index d92c3b5695..6e103ebbd7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeNode.java @@ -97,6 +97,7 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode { /** * Returns true if this nodes handles paste operations + * @param pastedNodes the nodes to be pasted * @return true if this nodes handles paste operations */ public abstract boolean canPaste(List pastedNodes); @@ -172,8 +173,7 @@ public abstract class SymbolTreeNode extends GTreeSlowLoadingNode { * @param monitor the task monitor * @return the node that contains the given symbol. */ - public GTreeNode findSymbolTreeNode(SymbolNode key, boolean loadChildren, - TaskMonitor monitor) { + 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()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeRootNode.java index 245d5520b5..aaadc294b0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeRootNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/nodes/SymbolTreeRootNode.java @@ -17,7 +17,6 @@ package ghidra.app.plugin.core.symboltree.nodes; import static ghidra.program.model.symbol.SymbolType.*; -import java.awt.datatransfer.DataFlavor; import java.util.*; import javax.swing.Icon; @@ -30,21 +29,31 @@ import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolType; import ghidra.util.task.TaskMonitor; -public class SymbolTreeRootNode extends SymbolCategoryNode { +public class SymbolTreeRootNode extends GTreeNode { private static Icon GLOBAL_ICON = new GIcon("icon.plugin.symboltree.node.root"); private final String name; - public SymbolTreeRootNode() { - name = "No Symbol Tree"; - } + protected SymbolCategory symbolCategory; + protected Program program; public SymbolTreeRootNode(Program program) { - super(SymbolCategory.ROOT_CATEGORY, program); - name = "Global"; + this.symbolCategory = SymbolCategory.ROOT_CATEGORY; + this.program = program; + + if (program == null) { + name = "No Symbol Tree"; + } + else { + name = "Global"; + } + } + + public Program getProgram() { + return program; } @Override - public List generateChildren(TaskMonitor monitor) { + public List generateChildren() { if (program == null) { return Collections.emptyList(); } @@ -61,7 +70,6 @@ public class SymbolTreeRootNode extends SymbolCategoryNode { return list; } - @Override public GTreeNode findSymbolTreeNode(SymbolNode key, boolean loadChildren, TaskMonitor monitor) { // @@ -93,7 +101,7 @@ public class SymbolTreeRootNode extends SymbolCategoryNode { } //else { GLOBAL, GLOBAL_VAR } // not sure where these end up - return super.findSymbolTreeNode(key, loadChildren, monitor); + return null; } private GTreeNode findCodeSymbol(SymbolNode key, boolean loadChildren, TaskMonitor monitor) { @@ -230,7 +238,6 @@ public class SymbolTreeRootNode extends SymbolCategoryNode { return null; // must be filtered out } - @Override public SymbolNode symbolAdded(Symbol symbol) { SymbolNode returnNode = null; List allChildren = getChildren(); @@ -244,7 +251,6 @@ public class SymbolTreeRootNode extends SymbolCategoryNode { return returnNode; } - @Override public void symbolRemoved(Symbol symbol, String oldName, TaskMonitor monitor) { // we have to loop--the symbol may exist in more than one category @@ -280,32 +286,14 @@ public class SymbolTreeRootNode extends SymbolCategoryNode { } @Override - public boolean canCut() { - return false; - } - - @Override - public boolean canPaste(List pastedNodes) { - return false; - } - - @Override - public DataFlavor getNodeDataFlavor() { - return null; - } - - @Override - public boolean isCut() { - return false; - } - - @Override - public boolean isModifiable() { - return false; - } - - @Override - public void setNodeCut(boolean isCut) { - throw new UnsupportedOperationException("Cannot cut the symbol tree root node"); + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SymbolTreeRootNode)) { + return false; + } + SymbolTreeRootNode node = (SymbolTreeRootNode) o; + return getName().equals(node.getName()); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolRowObject.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolRowObject.java index c1b2c279cf..75e14e3f9a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolRowObject.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolRowObject.java @@ -22,7 +22,7 @@ import ghidra.program.model.symbol.Symbol; /** * SymbolRowObject provides a lightweight {@link Symbol} - * table row object which may be used to reacquire an associated symbol. + * table row object which may be used to acquire an associated symbol. */ public class SymbolRowObject implements Comparable { @@ -50,7 +50,7 @@ public class SymbolRowObject implements Comparable { } /** - * Get symbol id used to reacquire symbol from program + * Get symbol id used to acquire symbol from program * @return symbol id */ public long getID() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java index 1554f0bdad..02d3dca934 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symtable/SymbolTableModel.java @@ -278,27 +278,26 @@ class SymbolTableModel extends AddressBasedTableModel { tool.setStatusInfo(""); List deleteList = new LinkedList<>(); - CompoundCmd cmd = new CompoundCmd("Delete symbol(s)"); + CompoundCmd cmd = new CompoundCmd<>("Delete symbol(s)"); for (Symbol symbol : rowObjects) { if (symbol.isDynamic()) { - continue;//can't delete dynamic symbols... + continue; // can't delete dynamic symbols... } deleteList.add(symbol); String label = symbol.getName(); + Address address = symbol.getAddress(); if (symbol.getSymbolType() == SymbolType.FUNCTION) { Function function = (Function) symbol.getObject(); boolean ignoreMissingFunction = function.isThunk(); - cmd.add(new DeleteFunctionCmd(symbol.getAddress(), ignoreMissingFunction)); + cmd.add(new DeleteFunctionCmd(address, ignoreMissingFunction)); if (symbol.getSource() != SourceType.DEFAULT) { // remove label which gets created when non-default function is removed - cmd.add(new DeleteLabelCmd(symbol.getAddress(), label, - symbol.getParentNamespace())); + cmd.add(new DeleteLabelCmd(address, label, symbol.getParentNamespace())); } } else { - cmd.add( - new DeleteLabelCmd(symbol.getAddress(), label, symbol.getParentNamespace())); + cmd.add(new DeleteLabelCmd(address, label, symbol.getParentNamespace())); } } if (cmd.size() == 0) { diff --git a/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java index c66e399158..48611f1352 100644 --- a/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/docking/widgets/tree/GTreeFilterTest.java @@ -62,8 +62,7 @@ public class GTreeFilterTest extends AbstractDockingTest { assertEquals(5, viewRoot().getChildCount()); setFilterText("ABC"); - assertEquals("Expected 4 of nodes to be in filtered tree!", 4, - viewRoot().getChildCount()); + assertEquals("Expected 4 of nodes to be in filtered tree!", 4, viewRoot().getChildCount()); checkContainsNode("ABC"); checkContainsNode("XABC"); @@ -441,13 +440,13 @@ public class GTreeFilterTest extends AbstractDockingTest { assertEquals(1, viewRoot().getChildCount()); Object originalValue = getInstanceField("uniquePreferenceKey", gTree); - setInstanceField("preferenceKey", gTree.getFilterProvider(), "XYZ"); + setInstanceField("uniquePreferenceKey", gTree, "XYZ"); setFilterOptions(TextFilterStrategy.STARTS_WITH, false); checkContainsNode("ABC"); checkContainsNode("ABCX"); assertEquals(2, viewRoot().getChildCount()); - setInstanceField("preferenceKey", gTree.getFilterProvider(), originalValue); + setInstanceField("uniquePreferenceKey", gTree, originalValue); setInstanceField("optionsSet", gTree.getFilterProvider(), false); restorePreferences(); checkContainsNode("ABC"); @@ -588,11 +587,11 @@ public class GTreeFilterTest extends AbstractDockingTest { private void setFilterOnPath(boolean usePath) { runSwing(() -> { - FilterOptions filterOptions = new FilterOptions(TextFilterStrategy.CONTAINS, - true, false, false, usePath, false, FilterOptions.DEFAULT_DELIMITER, - MultitermEvaluationMode.AND); - ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions( - filterOptions); + FilterOptions filterOptions = + new FilterOptions(TextFilterStrategy.CONTAINS, true, false, false, usePath, false, + FilterOptions.DEFAULT_DELIMITER, MultitermEvaluationMode.AND); + ((DefaultGTreeFilterProvider) gTree.getFilterProvider()) + .setFilterOptions(filterOptions); }); waitForTree(); } @@ -600,9 +599,8 @@ public class GTreeFilterTest extends AbstractDockingTest { private void restorePreferences() { runSwing(() -> { GTreeFilterProvider filterProvider = gTree.getFilterProvider(); - String key = (String) getInstanceField("uniquePreferenceKey", gTree); - Class[] classes = new Class[] { DockingWindowManager.class, String.class }; - Object[] objs = new Object[] { winMgr, key }; + Class[] classes = new Class[] { DockingWindowManager.class }; + Object[] objs = new Object[] { winMgr }; invokeInstanceMethod("loadFilterPreference", filterProvider, classes, objs); }); waitForTree(); @@ -639,8 +637,8 @@ public class GTreeFilterTest extends AbstractDockingTest { runSwing(() -> { FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted); - ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions( - filterOptions); + ((DefaultGTreeFilterProvider) gTree.getFilterProvider()) + .setFilterOptions(filterOptions); }); waitForTree(); } @@ -650,8 +648,8 @@ public class GTreeFilterTest extends AbstractDockingTest { runSwing(() -> { FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted, false, multiTerm, splitCharacter, evalMode); - ((DefaultGTreeFilterProvider) gTree.getFilterProvider()).setFilterOptions( - filterOptions); + ((DefaultGTreeFilterProvider) gTree.getFilterProvider()) + .setFilterOptions(filterOptions); }); waitForTree(); } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin4Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin4Test.java index 99a0cf8942..a2324c24aa 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin4Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin4Test.java @@ -17,10 +17,17 @@ package ghidra.app.plugin.core.symboltree; import static org.junit.Assert.*; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.TreePath; + import org.junit.*; import docking.action.DockingActionIf; -import docking.widgets.tree.GTreeNode; +import docking.widgets.filter.*; +import docking.widgets.tree.*; +import docking.widgets.tree.support.DepthFirstIterator; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.symboltree.nodes.SymbolNode; import ghidra.program.model.address.Address; @@ -259,10 +266,259 @@ public class SymbolTreePlugin4Test extends AbstractGhidraHeadedIntegrationTest { addr("0x1002cf9"), location.getAddress()); } + @Test + public void testClone() throws Exception { + + GTreeNode fNode = rootNode.getChild(2); + util.expandNode(fNode); + + GTreeNode gNode = fNode.getChild(1); + util.expandNode(gNode); + util.selectNode(gNode); + + DockingActionIf clone = getAction(plugin, "Symbol Tree Clone"); + performAction(clone); + + DisconnectedSymbolTreeProvider disconnectedProvider = + waitForComponentProvider(DisconnectedSymbolTreeProvider.class); + + GTree gTree = disconnectedProvider.getTree(); + TreePath selectedPath = gTree.getSelectionPath(); + GTreeNode selectedNode = (GTreeNode) selectedPath.getLastPathComponent(); + assertEquals(gNode, selectedNode); + } + + @Test + public void testClone_WithFilter() throws Exception { + + GTreeNode fNode = rootNode.getChild(2); + util.expandNode(fNode); + + GTreeNode gNode = fNode.getChild(1); + util.expandNode(gNode); + util.selectNode(gNode); + + SymbolGTree gTree = util.getTree(); + filter(gTree, "param_1"); + + assertEquals(6, countNodes(gTree)); + assertNodes(gTree, "ghidra", "doStuff"); + + DockingActionIf clone = getAction(plugin, "Symbol Tree Clone"); + performAction(clone); + + DisconnectedSymbolTreeProvider disconnectedProvider = + waitForComponentProvider(DisconnectedSymbolTreeProvider.class); + + GTree disconnectedGTree = disconnectedProvider.getTree(); + waitForTree(disconnectedGTree); + + TreePath selectedPath = disconnectedGTree.getSelectionPath(); + GTreeNode selectedNode = (GTreeNode) selectedPath.getLastPathComponent(); + assertEquals(gNode, selectedNode); + + assertFilterText(disconnectedGTree, "param_1"); + assertEquals(6, countNodes(disconnectedGTree)); + assertNodes(disconnectedGTree, "ghidra", "doStuff"); + } + + @Test + public void testClone_WithFilter_ChangedSettings() throws Exception { + + SymbolGTree gTree = util.getTree(); + setFilterOptions(gTree, TextFilterStrategy.MATCHES_EXACTLY, false); + filter(gTree, "param_1"); + + GTreeNode fNode = rootNode.getChild(2); + util.expandNode(fNode); + + GTreeNode gNode = fNode.getChild(1); + util.expandNode(gNode); + util.selectNode(gNode); + + assertEquals(6, countNodes(gTree)); + assertNodes(gTree, "ghidra", "doStuff"); + + DockingActionIf clone = getAction(plugin, "Symbol Tree Clone"); + performAction(clone); + + DisconnectedSymbolTreeProvider disconnectedProvider = + waitForComponentProvider(DisconnectedSymbolTreeProvider.class); + + GTree disconnectedGTree = disconnectedProvider.getTree(); + waitForTree(disconnectedGTree); + + TreePath selectedPath = disconnectedGTree.getSelectionPath(); + GTreeNode selectedNode = (GTreeNode) selectedPath.getLastPathComponent(); + assertEquals(gNode, selectedNode); + + assertFilterText(disconnectedGTree, "param_1"); + assertFilterSetting(disconnectedGTree, TextFilterStrategy.MATCHES_EXACTLY); + assertEquals(6, countNodes(disconnectedGTree)); + assertNodes(disconnectedGTree, "ghidra", "doStuff"); + } + + @Test + public void testClone_IgnoresProgramActivation() throws Exception { + + GTreeNode fNode = rootNode.getChild(2); + util.expandNode(fNode); + + GTreeNode gNode = fNode.getChild(1); + util.expandNode(gNode); + util.selectNode(gNode); + + DockingActionIf clone = getAction(plugin, "Symbol Tree Clone"); + performAction(clone); + + DisconnectedSymbolTreeProvider disconnectedProvider = + waitForComponentProvider(DisconnectedSymbolTreeProvider.class); + + GTree gTree = disconnectedProvider.getTree(); + TreePath selectedPath = gTree.getSelectionPath(); + GTreeNode selectedNode = (GTreeNode) selectedPath.getLastPathComponent(); + assertEquals(gNode, selectedNode); + + SymbolTreeProvider primaryProvider = util.getProvider(); + assertEquals(program, primaryProvider.getProgram()); + assertEquals(program, disconnectedProvider.getProgram()); + + Program program2 = util.openProgram2(); + + assertEquals(program2, primaryProvider.getProgram()); + assertEquals(program, disconnectedProvider.getProgram()); + } + + @Test + public void testClone_ClosingProgramClosesClonedProvider() throws Exception { + + DockingActionIf clone = getAction(plugin, "Symbol Tree Clone"); + performAction(clone); + + DisconnectedSymbolTreeProvider disconnectedProvider = + waitForComponentProvider(DisconnectedSymbolTreeProvider.class); + + SymbolTreeProvider primaryProvider = util.getProvider(); + assertEquals(program, primaryProvider.getProgram()); + assertEquals(program, disconnectedProvider.getProgram()); + + Program program2 = util.openProgram2(); + + assertEquals(program2, primaryProvider.getProgram()); + assertEquals(program, disconnectedProvider.getProgram()); + + util.closeProgram(); + + assertTrue(primaryProvider.isVisible()); + assertFalse(disconnectedProvider.isVisible()); + + assertEquals(program2, primaryProvider.getProgram()); + assertNull(disconnectedProvider.getProgram()); + } + //================================================================================================== // Private Methods //================================================================================================== + private void setFilterOptions(GTree gTree, TextFilterStrategy filterStrategy, + boolean inverted) { + + runSwing(() -> { + FilterOptions filterOptions = new FilterOptions(filterStrategy, false, false, inverted); + ((DefaultGTreeFilterProvider) gTree.getFilterProvider()) + .setFilterOptions(filterOptions); + }); + waitForTree(gTree); + } + + private void assertFilterSetting(GTree gTree, TextFilterStrategy expectedStrategy) { + + FilterOptions filterOptions = runSwing(() -> { + DefaultGTreeFilterProvider provider = + ((DefaultGTreeFilterProvider) gTree.getFilterProvider()); + return provider.getFilterOptions(); + }); + + assertEquals(expectedStrategy, filterOptions.getTextFilterStrategy()); + } + + private int countNodes(GTree gTree) { + int n = 0; + DepthFirstIterator it = new DepthFirstIterator(gTree.getViewRoot()); + while (it.hasNext()) { + n++; + it.next(); + } + return n; + } + + private void filter(GTree gTree, String text) { + + FilterTextField filterField = (FilterTextField) gTree.getFilterField(); + runSwing(() -> { + filterField.setText(text); + }); + waitForTree(gTree); + } + + private void assertFilterText(GTree gTree, String expectedText) { + FilterTextField filterField = (FilterTextField) gTree.getFilterField(); + String filterText = runSwing(() -> filterField.getText()); + assertEquals(expectedText, filterText); + } + + private void assertNodes(GTree gTree, String... nodeNames) { + + List nodes = new ArrayList<>(); + for (String name : nodeNames) { + GTreeNode node = node(name); + assertNotNull(node); + nodes.add(node); + } + + int count = 0; + int rows = gTree.getRowCount(); + for (int i = 0; i < rows; i++) { + TreePath path = gTree.getPathForRow(i); + GTreeNode node = (GTreeNode) path.getLastPathComponent(); + if (node.isLeaf()) { + if (node.isLeaf()) { + count++; + } + } + } + + assertEquals(nodes.size(), count); + for (GTreeNode node : nodes) { + TreePath path = node.getTreePath(); + assertTrue("Could not find row for path: " + path, gTree.getRowForPath(path) != -1); + } + } + + private GTreeNode node(String name) { + return findNodeInTree(rootNode, name); + } + + private GTreeNode findNodeInTree(GTreeNode node, String name) { + if (node.getName().equals(name)) { + return node; + } + + List children = node.getChildren(); + for (GTreeNode child : children) { + if (child.getName().startsWith(name)) { + return child; + } + + GTreeNode grandChild = findNodeInTree(child, name); + if (grandChild != null) { + return grandChild; + } + } + + return null; + } + private Address addr(String address) { return program.getAddressFactory().getAddress(address); } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreeTestUtils.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreeTestUtils.java index f903b985a8..7f2a135198 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreeTestUtils.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/symboltree/SymbolTreeTestUtils.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.symboltree; import static generic.test.AbstractGTest.*; import static generic.test.AbstractGenericTest.*; +import static generic.test.AbstractGuiTest.*; import static ghidra.test.AbstractGhidraHeadedIntegrationTest.*; import static org.junit.Assert.*; @@ -102,7 +103,7 @@ class SymbolTreeTestUtils { public static Program buildProgram() throws Exception { - ToyProgramBuilder builder = new ToyProgramBuilder("notepad", true); + ToyProgramBuilder builder = new ToyProgramBuilder("sample1", true); Program program = builder.getProgram(); builder.createMemory("test", "0x1001000", 0x5500); @@ -169,6 +170,49 @@ class SymbolTreeTestUtils { return program; } + public static Program buildProgram2() throws Exception { + + // Note: the contents of this program are arbitrary and loosely based off of the program + // in buildProgram(). + + ToyProgramBuilder builder = new ToyProgramBuilder("sample2", true); + Program program = builder.getProgram(); + + builder.createMemory("test", "0x1001000", 0x5500); + + // create an 'Exports' node + builder.createEntryPoint("0x1006420", "entry"); + builder.createLabel("0x1006420", "entry"); + + // imports symbol tree node + builder.createExternalLibraries("ADVAPI32.dll", "comdlg32.dll", "GDI32.dll", "KERNEL32.dll", + "MSVCRT.dll", "SHELL32.dll", "USER32.dll", "WINSPOOL.DRV"); + builder.createExternalReference("0x1001000", "ADVAPI32.dll", "IsTextUnicode", 0); + builder.createLabel("0x1001000", "ADVAPI32.dll_IsTextUnicode"); + builder.createExternalReference("0x1001004", "ADVAPI32.dll", "RegCreateKeyW", 0); + + ExternalManager externalManager = builder.getProgram().getExternalManager(); + int tx = program.startTransaction("Test Transaction"); + externalManager.setExternalPath("ADVAPI32.dll", "/path/to/ADVAPI32.DLL", true); + program.endTransaction(tx, true); + + // functions + builder.createEmptyFunction("doStuff2", null, "0x10048a3", 19, new Undefined1DataType(), + new ParameterImpl("param_1", new IntegerDataType(), program), + new ParameterImpl("param_2", new IntegerDataType(), program)); + + //@formatter:off + ParameterImpl p = new ParameterImpl(null /*auto name*/, new IntegerDataType(), program); + builder.createEmptyFunction("ghidra2", null, "0x1002cf5", 121, new Undefined1DataType(), + p, p, p, p, p, p, p, p, p); + //@formatter:on + + builder.createLabel("0x1002d2b", "AnotherLoca2l", "ghidra"); + builder.createLabel("0x1002d1f", "MyLocal2", "ghidra"); + + return program; + } + SymbolTreeRootNode getRootNode() { return (SymbolTreeRootNode) rootGTreeNode; } @@ -359,8 +403,8 @@ class SymbolTreeTestUtils { } void closeProgram() throws Exception { - final ProgramManager pm = plugin.getTool().getService(ProgramManager.class); - runSwing(() -> pm.closeProgram()); + ProgramManager pm = plugin.getTool().getService(ProgramManager.class); + runSwing(() -> pm.closeProgram(program, true)); } Program getProgram() { @@ -372,6 +416,13 @@ class SymbolTreeTestUtils { pm.openProgram(program.getDomainFile()); } + Program openProgram2() throws Exception { + Program p2 = buildProgram2(); + ProgramManager pm = plugin.getTool().getService(ProgramManager.class); + pm.openProgram(p2.getDomainFile()); + return p2; + } + void clearClipboard() { Clipboard clipboard = (Clipboard) getInstanceField("localClipboard", provider); ClipboardOwner owner = (ClipboardOwner) getInstanceField("clipboardOwner", provider); diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java index cc3c2214cd..91cb84ee0c 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java @@ -662,7 +662,7 @@ class FGActionManager { Icon image = new GIcon("icon.plugin.functiongraph.action.viewer.clone"); cloneAction.setToolBarData(new ToolBarData(image, toolbarEndGroup)); cloneAction.setDescription( - "Create a snapshot (disconnected) copy of this Function Graph window "); + "Create a snapshot (disconnected) copy of this Function Graph window"); cloneAction.setHelpLocation(new HelpLocation("Snapshots", "Snapshots_Start")); cloneAction.setHelpLocation( new HelpLocation("FunctionGraphPlugin", "Function_Graph_Action_Snapshot")); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java index 73bc18f722..172f3937b2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filter/FilterTextField.java @@ -68,6 +68,8 @@ public class FilterTextField extends JPanel { private WeakSet listeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); private WeakSet enterListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); + private String accessibleNamePrefix; + /** * Constructs this text field with the given component. component may be null, but * then this field will be unable to flash in response to focus events (see the header @@ -302,6 +304,27 @@ public class FilterTextField extends JPanel { } } + /** + * Sets the accessible name prefix for for the focusable components in the filter panel. + * @param prefix the base name for these components. A suffix will be added to further + * describe the sub component. + */ + public void setAccessibleNamePrefix(String prefix) { + this.accessibleNamePrefix = prefix; + String name = prefix + " filter text field"; + textField.setName(name); + textField.getAccessibleContext().setAccessibleName(name); + } + + /** + * Returns the accessible name prefix set by a previous call to + * {@link #setAccessibleNamePrefix(String)}. This will be null if not set. + * @return the prefix + */ + public String getAccessibleNamePrefix() { + return accessibleNamePrefix; + } + //================================================================================================== // Package Methods (these make testing easier) //================================================================================================== @@ -374,6 +397,7 @@ public class FilterTextField extends JPanel { }); } + //================================================================================================== // Inner Classes //================================================================================================== @@ -463,17 +487,4 @@ public class FilterTextField extends JPanel { flashCount = 0; } } - - /** - * Sets the accessible name prefix for for the focusable components in the filter panel. - * @param prefix the base name for these components. A suffix will be added to further - * describe the sub component. - */ - public void setAccessibleNamePrefix(String prefix) { - String name = prefix + " filter text field"; - textField.setName(name); - textField.getAccessibleContext().setAccessibleName(name); - - } - } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java index b1ac603fb2..bddba92fb6 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java @@ -46,7 +46,6 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { private JPanel filterPanel; private FilterTransformer dataTransformer = new DefaultGTreeDataTransformer(); - private String preferenceKey; private boolean optionsSet; public DefaultGTreeFilterProvider(GTree gTree) { @@ -55,6 +54,28 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { filterPanel = createFilterPanel(); } + @Override + public GTreeFilterProvider copy(GTree newTree) { + DefaultGTreeFilterProvider newProvider = new DefaultGTreeFilterProvider(newTree); + + FilterOptions existingOptions = filterFactory.getFilterOptions(); + newProvider.setFilterOptions(existingOptions); + + String existingText = filterField.getText(); + newProvider.setFilterText(existingText); + + if (!filterField.isEnabled()) { + newProvider.setEnabled(false); + } + + String accessibleNamePrefix = filterField.getAccessibleNamePrefix(); + if (accessibleNamePrefix != null) { + newProvider.setAccessibleNamePrefix(accessibleNamePrefix); + } + + return newProvider; + } + @Override public JComponent getFilterComponent() { return filterPanel; @@ -90,10 +111,14 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { // tooltips which seem excessive to read to the user every time they get focus. We may need // to revisit this decision. context.setAccessibleDescription(""); - } private void updateModelFilter() { + + FilterOptions filterOptions = filterFactory.getFilterOptions(); + filterStateButton.setIcon(filterOptions.getFilterStateIcon()); + filterStateButton.setToolTipText(filterOptions.getFilterDescription()); + gTree.filterChanged(); } @@ -103,7 +128,7 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { DockingWindowManager dwm = DockingWindowManager.getInstance(gTree.getJTree()); if (dwm != null) { - dwm.putPreferenceState(preferenceKey, preferenceState); + dwm.putPreferenceState(gTree.getPreferenceKey(), preferenceState); } } @@ -114,10 +139,12 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { updateModelFilter(); } + public FilterOptions getFilterOptions() { + return filterFactory.getFilterOptions(); + } + @Override - public void loadFilterPreference(DockingWindowManager windowManager, - String uniquePreferenceKey) { - preferenceKey = uniquePreferenceKey; + public void loadFilterPreference(DockingWindowManager windowManager) { if (optionsSet) { // if the options were specifically set, don't restore saved values return; } @@ -126,12 +153,12 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { return; } - PreferenceState preferenceState = windowManager.getPreferenceState(preferenceKey); - if (preferenceState == null) { + PreferenceState state = windowManager.getPreferenceState(gTree.getPreferenceKey()); + if (state == null) { return; } - Element xmlElement = preferenceState.getXmlElement(FILTER_STATE); + Element xmlElement = state.getXmlElement(FILTER_STATE); if (xmlElement != null) { FilterOptions filterOptions = FilterOptions.restoreFromXML(xmlElement); filterFactory = new GTreeFilterFactory(filterOptions); @@ -157,10 +184,6 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { FilterOptions newFilterOptions = dialog.getResultFilterOptions(); if (newFilterOptions != null) { filterFactory = new GTreeFilterFactory(newFilterOptions); - - filterStateButton.setIcon(newFilterOptions.getFilterStateIcon()); - filterStateButton.setToolTipText(newFilterOptions.getFilterDescription()); - saveFilterState(); updateModelFilter(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java index 3e80443f06..818a13ab0e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java @@ -139,8 +139,7 @@ public class GTree extends JPanel implements BusyListener { init(); DockingWindowManager.registerComponentLoadedListener(this, - (windowManager, provider) -> filterProvider.loadFilterPreference(windowManager, - uniquePreferenceKey)); + (windowManager, provider) -> filterProvider.loadFilterPreference(windowManager)); filterUpdateManager = new SwingUpdateManager(1000, 30000, () -> updateModelFilter()); Gui.addThemeListener(themeListener); @@ -385,6 +384,17 @@ public class GTree extends JPanel implements BusyListener { runTask(new GTreeRestoreTreeStateTask(this, state)); } + /** + * Sets the filter restore state. This method is a way to override the tree's filtering + * behavior, which is usually set by a call to {@link #saveFilterRestoreState()}. Most clients + * will never need to call this method. + * + * @param state the state to set + */ + protected void setFilterRestoreState(GTreeState state) { + this.filterRestoreTreeState = state; + } + /** * Signal to the tree that it should record its expanded and selected state when a new filter is * applied @@ -404,6 +414,14 @@ public class GTree extends JPanel implements BusyListener { filterRestoreTreeState = null; } + /** + * Returns the key that this tree uses to store preferences. + * @return the key that this tree uses to store preferences. + */ + public String getPreferenceKey() { + return uniquePreferenceKey; + } + /** * A method that subclasses can use to be notified when tree state has been restored. This * method is called after a major structural tree change has happened and the paths that @@ -689,7 +707,8 @@ public class GTree extends JPanel implements BusyListener { return node; // this node is a valid child of the given root } - GTreeNode parentNode = getNodeForPath(root, path.getParentPath()); + TreePath parentPath = path.getParentPath(); + GTreeNode parentNode = getNodeForPath(root, parentPath); if (parentNode == null) { return null; // must be a path we don't have } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java index 8eb01aea1b..1d2bb74e74 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java @@ -26,7 +26,7 @@ import ghidra.util.FilterTransformer; */ public interface GTreeFilterProvider { /** - * Returns the component to place at the bottom of a GTree to provider filtering capabilites. + * Returns the component to place at the bottom of a GTree to provider filtering capabilities. * @return the filter component */ public JComponent getFilterComponent(); @@ -65,10 +65,8 @@ public interface GTreeFilterProvider { /** * Loads any filter preferences that have been saved. * @param windowManager the {@link DockingWindowManager} to load preferences from - * @param uniquePreferenceKey the preference key */ - public void loadFilterPreference(DockingWindowManager windowManager, - String uniquePreferenceKey); + public void loadFilterPreference(DockingWindowManager windowManager); /** * Sets an accessible name on the filter component. This prefix will be used to assign @@ -82,4 +80,18 @@ public interface GTreeFilterProvider { * example if the tree contains fruits, then "Fruits" would be an appropriate prefix name. */ public void setAccessibleNamePrefix(String namePrefix); + + /** + * Creates a copy of this filter with all current filter settings. + *

+ * This is meant to be used for GTrees that support creating a new copy. + *

+ * Note: Filter providers that do not support copying will return null from this method. + * + * @param gTree the new tree for the new filter + * @return the copy + */ + public default GTreeFilterProvider copy(GTree gTree) { + return null; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java index e0d7641e9f..86ab550c52 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java @@ -388,7 +388,6 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable list = new ArrayList<>(); - if (isLoaded()) { for (GTreeNode child : children()) { monitor.checkCancelled(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java index 0b85eca17b..67d2942d6e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java @@ -72,7 +72,7 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent setText(text); setToolTipText(node.getToolTip()); - Icon icon = node.getIcon(expanded); + Icon icon = getNodeIcon(node, expanded); if (icon == null) { icon = getIcon(); } @@ -90,6 +90,10 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent return this; } + protected Icon getNodeIcon(GTreeNode node, boolean expanded) { + return node.getIcon(expanded); + } + /** * Overrides this method to ensure that the new background selection color is not * a {@link GColorUIResource}. Some Look and Feels will ignore color values that extend diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeExpandPathsTask.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeExpandPathsTask.java index c34365bf90..34e5f536f9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeExpandPathsTask.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/tasks/GTreeExpandPathsTask.java @@ -52,17 +52,18 @@ public class GTreeExpandPathsTask extends GTreeTask { if (nodeList.length < 2) { return; // only the root is in the path } + List allChildren = parent.getChildren(); for (int i = 1; i < nodeList.length; i++) { if (monitor.isCancelled()) { return; } - GTreeNode node = findNode(allChildren, (GTreeNode) nodeList[i]); - if (node == null) { + + GTreeNode nextParent = findNode(allChildren, (GTreeNode) nodeList[i]); + if (nextParent == null) { return; } - allChildren = node.getChildren(); - parent = node; + allChildren = nextParent.getChildren(); } }