GP-3849 - Symbol Tree - Added snapshot feature

This commit is contained in:
dragonmacher 2024-05-04 10:01:40 -04:00
parent c014e6851f
commit 6e255143fb
38 changed files with 1140 additions and 256 deletions

View File

@ -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|

View File

@ -348,7 +348,7 @@
</BLOCKQUOTE>
<H2>View Qualified Names in Code Browser</H2>
<BLOCKQUOTE>
<P>To include namespace names in the display of labels and names within the Code Browser,
select <B>Edit</B> <IMG alt="" src="help/shared/arrow.gif"> <B>Tool Options...</B> from the
@ -359,6 +359,46 @@
"help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm">CodeBrowser Options</A>).<BR>
</P>
</BLOCKQUOTE>
<H2>Symbol Tree Snapshots</H2>
<BLOCKQUOTE>
<P>Pressing the <IMG SRC="icon.provider.clone" /> 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.
</P>
<H3><A name="Symbol_Tree_Clone"></A>Symbol Tree Clone Action <IMG SRC="icon.provider.clone" /></H3>
<BLOCKQUOTE>
<P>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.</P>
</BLOCKQUOTE>
<H3><A name="Disable_Category"></A>Disable Category</H3>
<BLOCKQUOTE>
<P>When working in a snapshot Symbol Tree, you can choose to disable a root-level folder
by right-clicking and selected <B>Disable Category</B>. Once disabled, the node will remain
in the tree with a disabled icon. A disabled node will no longer show any children.
</P>
</BLOCKQUOTE>
<H3><A name="Enable_Category"></A>Enable Category</H3>
<BLOCKQUOTE>
<P>This action is used to re-enable categories that were previously disabled.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided By: <I>SymbolTreePlugin</I></P>

View File

@ -21,8 +21,6 @@ import ghidra.util.exception.InvalidInputException;
/**
* Command for setting the external program name and path.
*
*
*/
public class SetExternalNameCmd implements Command<Program> {
@ -34,7 +32,7 @@ public class SetExternalNameCmd implements Command<Program> {
/**
* 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<Program> cmd = new SetExternalNameCmd(extProgName, domainFile.getPathname());
tool.execute(cmd, program);
}
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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<SymbolNode> getSelectedSymbolNodes() {
if (selectionPaths == null) {
return List.of();
}
List<SymbolNode> symbols = new ArrayList<>();
for (TreePath treePath : selectionPaths) {
Object object = treePath.getLastPathComponent();
if (object instanceof SymbolNode) {
symbols.add((SymbolNode) object);
}
}
return symbols;
}
private static List<Symbol> getSymbols(TreePath[] selectionPaths) {
if (selectionPaths == null) {
return null;

View File

@ -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<SymbolTreeProvider> 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<SymbolTreeProvider> 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<SymbolTreeProvider> 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;
}
}

View File

@ -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<Symbol> symbols = symbolTable.getSymbols(symbol.getName(),
destinationNamespace);
List<Symbol> 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;
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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();

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<GTreeNode> myChildren = getChildren();
List<GTreeNode> 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<GTreeNode> 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;
}
}

View File

@ -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<GTreeNode> generateChildren(TaskMonitor monitor) {
List<GTreeNode> list = new ArrayList<>();
if (!isEnabled) {
return Collections.emptyList();
}
List<GTreeNode> list = new ArrayList<>();
List<Symbol> functionSymbolList = getExportSymbols();
for (Symbol symbol : functionSymbolList) {
list.add(SymbolNode.createNode(symbol, program));

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<GTreeNode> generateChildren(TaskMonitor monitor) throws CancelledException {
if (!isEnabled) {
return Collections.emptyList();
}
SymbolType symbolType = symbolCategory.getSymbolType();
List<GTreeNode> list = getSymbols(symbolType, monitor);
monitor.checkCancelled();

View File

@ -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<GTreeNode> 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()) {

View File

@ -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<GTreeNode> generateChildren(TaskMonitor monitor) {
public List<GTreeNode> 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<GTreeNode> 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<GTreeNode> 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());
}
}

View File

@ -22,7 +22,7 @@ import ghidra.program.model.symbol.Symbol;
/**
* <code>SymbolRowObject</code> 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<SymbolRowObject> {
@ -50,7 +50,7 @@ public class SymbolRowObject implements Comparable<SymbolRowObject> {
}
/**
* Get symbol id used to reacquire symbol from program
* Get symbol id used to acquire symbol from program
* @return symbol id
*/
public long getID() {

View File

@ -278,27 +278,26 @@ class SymbolTableModel extends AddressBasedTableModel<SymbolRowObject> {
tool.setStatusInfo("");
List<Symbol> deleteList = new LinkedList<>();
CompoundCmd cmd = new CompoundCmd("Delete symbol(s)");
CompoundCmd<Program> 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) {

View File

@ -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();
}

View File

@ -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<GTreeNode> 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<GTreeNode> 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);
}

View File

@ -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);

View File

@ -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"));

View File

@ -68,6 +68,8 @@ public class FilterTextField extends JPanel {
private WeakSet<FilterListener> listeners = WeakDataStructureFactory.createCopyOnWriteWeakSet();
private WeakSet<Callback> enterListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet();
private String accessibleNamePrefix;
/**
* Constructs this text field with the given component. <code>component</code> 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);
}
}

View File

@ -46,7 +46,6 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider {
private JPanel filterPanel;
private FilterTransformer<GTreeNode> 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();
}

View File

@ -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 <b>and</b> 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
}

View File

@ -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.
* <P>
* This is meant to be used for GTrees that support creating a new copy.
* <P>
* 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;
}
}

View File

@ -388,7 +388,6 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
public GTreeNode filter(GTreeFilter filter, TaskMonitor monitor)
throws CancelledException, CloneNotSupportedException {
List<GTreeNode> list = new ArrayList<>();
if (isLoaded()) {
for (GTreeNode child : children()) {
monitor.checkCancelled();

View File

@ -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

View File

@ -52,17 +52,18 @@ public class GTreeExpandPathsTask extends GTreeTask {
if (nodeList.length < 2) {
return; // only the root is in the path
}
List<GTreeNode> 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();
}
}