Adding capbility to add DockingActions to GraphDisplay. Also, wired in the help for existing actions.

This commit is contained in:
ghidravore 2020-10-07 17:58:48 -04:00
parent b647c6cd5b
commit 532a1d4fd0
24 changed files with 1142 additions and 533 deletions

View file

@ -20,10 +20,7 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import docking.widgets.EventTrigger;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.app.events.*;
import ghidra.framework.cmd.Command;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool;
@ -42,7 +39,7 @@ import ghidra.util.Swing;
public abstract class AddressBasedGraphDisplayListener
implements GraphDisplayListener, PluginEventListener, DomainObjectListener {
private PluginTool tool;
protected PluginTool tool;
private GraphDisplay graphDisplay;
protected Program program;
private SymbolTable symbolTable;
@ -168,21 +165,6 @@ public abstract class AddressBasedGraphDisplayListener
return p == program;
}
@Override
public boolean updateVertexName(String vertexId, String oldName, String newName) {
Address address = getAddressForVertexId(vertexId);
Symbol symbol = program.getSymbolTable().getPrimarySymbol(address);
Command command;
if (symbol != null) {
command = new RenameLabelCmd(address, oldName, newName, SourceType.USER_DEFINED);
}
else {
command = new AddLabelCmd(address, newName, SourceType.USER_DEFINED);
}
return tool.execute(command, program);
}
@Override
public void domainObjectChanged(DomainObjectChangedEvent ev) {
if (!(ev.containsEvent(ChangeManager.DOCR_SYMBOL_ADDED) ||

View file

@ -39,6 +39,9 @@ import ghidra.util.HelpLocation;
import ghidra.util.exception.AssertException;
import ghidra.util.layout.VerticalLayout;
/**
* Dialog used to a label or to edit an existing label.
*/
public class AddEditDialog extends DialogComponentProvider {
private static final int MAX_RETENTION = 10;
private PluginTool tool;
@ -69,6 +72,72 @@ public class AddEditDialog extends DialogComponentProvider {
setDefaultButton(okButton);
}
/**
* Invokes the dialog to add a new label in the given program at the given address
* @param address the address at which to add a new label
* @param prog the program in which to add a new label
*/
public void addLabel(Address address, Program prog) {
addLabel(address, prog, tool.getActiveWindow());
}
/**
* Invokes the dialog to add a new label in the given program at the given address
* @param address the address at which to add a new label
* @param targetProgram the program in which to add a new label
* @param provider the ComponentProvider to parent and center the dialog over.
*/
public void addLabel(Address address, Program targetProgram, ComponentProvider provider) {
initDialogForAdd(targetProgram, address);
tool.showDialog(this, provider);
}
/**
* Invokes the dialog to add a new label in the given program at the given address
* @param address the address at which to add a new label
* @param targetProgram the program in which to add a new label
* @param centeredOverComponent the component over which to center the dialog
*/
public void addLabel(Address address, Program targetProgram, Component centeredOverComponent) {
initDialogForAdd(targetProgram, address);
tool.showDialog(this, centeredOverComponent);
}
/**
* Invokes the dialog to edit an existing label in the given program
* @param targetSymbol the symbol(label) to edit
* @param targetProgram the program containing the symbol
*/
public void editLabel(Symbol targetSymbol, Program targetProgram) {
ComponentProvider componentProvider =
tool.getComponentProvider(PluginConstants.CODE_BROWSER);
JComponent component = componentProvider.getComponent();
editLabel(targetSymbol, targetProgram, component);
}
/**
* Invokes the dialog to edit an existing label in the given program
* @param targetSymbol the symbol(label) to edit
* @param targetProgram the program containing the symbol
* @param centeredOverComponent the component over which to center the dialog
*/
public void editLabel(Symbol targetSymbol, Program targetProgram,
Component centeredOverComponent) {
initDialogForEdit(targetProgram, targetSymbol);
tool.showDialog(this, centeredOverComponent);
}
/**
* Invokes the dialog to edit an existing label in the given program
* @param targetSymbol the symbol(label) to edit
* @param targetProgram the program containing the symbol
* @param provider the ComponentProvider to parent and center the dialog over.
*/
public void editLabel(Symbol targetSymbol, Program targetProgram, ComponentProvider provider) {
initDialogForEdit(targetProgram, targetSymbol);
tool.showDialog(this, provider);
}
@Override
protected void okCallback() {
@ -134,28 +203,6 @@ public class AddEditDialog extends DialogComponentProvider {
return new SymbolPath(symbolName);
}
private boolean isLocalNamespace(Namespace namespace, String symbolName) {
FunctionSymbol functionSymbol = getFunctionSymbol(addr);
if (functionSymbol == null) {
return false;
}
if (!isInFunctionNamespace(namespace)) {
return false;
}
return true;
}
private boolean isInFunctionNamespace(Namespace namespace) {
for (Namespace p = namespace; p != null; p = p.getParentNamespace()) {
if (p instanceof Function) {
return true;
}
}
return false;
}
private Namespace getSelectedNamespace() {
Object selectedItem = namespaceChoices.getSelectedItem();
if (selectedItem == null) {
@ -240,7 +287,8 @@ public class AddEditDialog extends DialogComponentProvider {
Namespace currentNamespace = program.getSymbolTable().getNamespace(addr);
// no symbol or not editing a function symbol
if ((symbol == null) || (symbol != null && symbol.getSymbolType() != SymbolType.FUNCTION)) {
if ((symbol == null) ||
(symbol != null && symbol.getSymbolType() != SymbolType.FUNCTION)) {
// walk the tree of namespaces and collect all of the items
for (; (currentNamespace != globalNamespace); currentNamespace =
currentNamespace.getParentNamespace()) {
@ -277,7 +325,8 @@ public class AddEditDialog extends DialogComponentProvider {
*/
private void selectNamespace() {
if (symbol != null && symbol.getParentNamespace() != null) {
namespaceChoices.setSelectedItem(new NamespaceWrapper(symbol.getParentNamespace()));
namespaceChoices
.setSelectedItem(new NamespaceWrapper(symbol.getParentNamespace()));
return;
}
@ -330,18 +379,14 @@ public class AddEditDialog extends DialogComponentProvider {
return null;
}
public void addLabel(Address address, Program p) {
addLabel(address, p, tool.getActiveWindow());
}
public void addLabel(Address address, Program p, Component centeredOverComponent) {
private void initDialogForAdd(Program p, Address address) {
if (!address.isMemoryAddress()) {
throw new IllegalArgumentException(
"AddEditDialog.addLabel only valid for memory address");
}
this.addr = address;
this.program = p;
SymbolTable symbolTable = program.getSymbolTable();
SymbolTable symbolTable = p.getSymbolTable();
symbol = null;
setTitle("Add Label at " + address);
initRecentChoices();
@ -365,17 +410,10 @@ public class AddEditDialog extends DialogComponentProvider {
namespaceChoices.setEnabled(true);
initNamespaces();
clearStatusText();
tool.showDialog(this, centeredOverComponent);
}
public void editLabel(Symbol s, Program p) {
ComponentProvider componentProvider =
tool.getComponentProvider(PluginConstants.CODE_BROWSER);
JComponent component = componentProvider.getComponent();
editLabel(s, p, component);
}
public void editLabel(Symbol s, Program p, Component centeredOverComponent) {
private void initDialogForEdit(Program p, Symbol s) {
this.symbol = s;
this.program = p;
this.addr = s.getAddress();
@ -386,7 +424,8 @@ public class AddEditDialog extends DialogComponentProvider {
if (s.getSymbolType() == SymbolType.FUNCTION) {
String title;
if (s.isExternal()) {
ExternalLocation extLoc = p.getExternalManager().getExternalLocation(s);
ExternalLocation extLoc =
program.getExternalManager().getExternalLocation(s);
Address fnAddr = extLoc.getAddress();
title = "Rename External Function";
if (fnAddr != null) {
@ -433,7 +472,7 @@ public class AddEditDialog extends DialogComponentProvider {
}
initNamespaces();
clearStatusText();
tool.showDialog(this, centeredOverComponent);
}
/**

View file

@ -169,7 +169,8 @@ public class GraphAST extends GhidraScript {
return vert;
}
protected AttributedVertex getVarnodeVertex(Map<Integer, AttributedVertex> vertices, VarnodeAST vn) {
protected AttributedVertex getVarnodeVertex(Map<Integer, AttributedVertex> vertices,
VarnodeAST vn) {
AttributedVertex res;
res = vertices.get(vn.getUniqueId());
if (res == null) {
@ -259,5 +260,10 @@ public class GraphAST extends GhidraScript {
String addrString = vertexId.substring(0, firstSpace);
return getAddress(addrString);
}
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new ASTGraphDisplayListener(tool, graphDisplay, highfunc, currentProgram);
}
}
}

View file

@ -27,6 +27,7 @@ import ghidra.program.model.address.*;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.PcodeBlockBasic;
import ghidra.service.graph.GraphDisplay;
import ghidra.service.graph.GraphDisplayListener;
import ghidra.util.exception.AssertException;
/**
@ -112,4 +113,9 @@ public class ASTGraphDisplayListener extends AddressBasedGraphDisplayListener {
}
}
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new ASTGraphDisplayListener(tool, graphDisplay, hfunction, graphType);
}
}

View file

@ -36,6 +36,7 @@ public class ASTGraphTask extends Task {
enum GraphType {
CONTROL_FLOW_GRAPH("AST Control Flow"), DATA_FLOW_GRAPH("AST Data Flow");
private String name;
GraphType(String name) {
this.name = name;
}
@ -194,7 +195,8 @@ public class ASTGraphTask extends Task {
}
}
private AttributedVertex getOpVertex(AttributedGraph graph, PcodeOpAST op, TaskMonitor monitor) {
private AttributedVertex getOpVertex(AttributedGraph graph, PcodeOpAST op,
TaskMonitor monitor) {
String key = "O_" + Integer.toString(op.getSeqnum().getTime());
AttributedVertex vertex = graph.getVertex(key);
@ -226,7 +228,8 @@ public class ASTGraphTask extends Task {
vertex.setAttribute(VERTEX_TYPE_ATTRIBUTE, vertexType);
}
private AttributedVertex getDataVertex(AttributedGraph graph, Varnode node, TaskMonitor monitor) {
private AttributedVertex getDataVertex(AttributedGraph graph, Varnode node,
TaskMonitor monitor) {
// TODO: Missing Varnode unique ID ??
@ -288,7 +291,8 @@ public class ASTGraphTask extends Task {
}
}
private AttributedVertex getBlockVertex(AttributedGraph graph, PcodeBlock pblock, TaskMonitor monitor) {
private AttributedVertex getBlockVertex(AttributedGraph graph, PcodeBlock pblock,
TaskMonitor monitor) {
String key = Integer.toString(pblock.getIndex());
AttributedVertex vertex = graph.getVertex(key);

View file

@ -37,44 +37,71 @@
<li>Ctrl+MouseButton1+drag on an empty area will create a rectangular area and select enclosed vertices</li>
<li>Ctrl+MouseButton1+drag over a vertex will reposition all selected vertices</li>
</ul>
<H2>Upper-right Icon Buttons:</H2>
<ul>
<li>The <IMG src="images/locationIn.gif"> toggle button, when 'set' will cause a located vertex (red arrow) to be scrolled to the center of the view</li>
<li>The <IMG src="images/Lasso.png" width="16" height="16"> toggle button, when 'set' will allow the user to draw a free-form shape that encloses the vertices they wish to select.
<li>The <IMG src="images/sat2.png" width="16" height="16"> toggle button, when 'set' will open a satellite mini view of the graph in the lower right corner. The mini-view can be manipulated with the mouse to affect the main view</li>
<li>The <IMG src="images/reload3.png"> button will reset any visual transformations on the graph and center it at a best-effort size</li>
<li>The <IMG src="images/magnifier.png"> toggle button, when 'set' will open a rectangular magnification lens in the graph view</li>
<H2>Toolbar Buttons</H2>
<P><A name="Scroll_To_Selection"/> The <IMG src="images/locationIn.gif"> toggle button, when 'set' will cause a focused vertex (red arrow) to be scrolled to the center of the view</P>
<P><A name="Free_Form_Selection"/>The <IMG src="images/Lasso.png" width="16" height="16"> toggle button, when 'set' will allow the user to draw a free-form shape that encloses the vertices they wish to select.</P>
<P><A name="SatelliteView"/>The <IMG src="images/sat2.png" width="16" height="16"> toggle button, when 'set' will open a satellite mini view of the graph in the lower right corner. The mini-view can be manipulated with the mouse to affect the main view</P>
<P><A name="Reset_View"/>The <IMG src="images/reload3.png"> button will reset any visual transformations on the graph and center it at a best-effort size</P>
<P><A name="View_Magnifier"/>The <IMG src="images/magnifier.png"> toggle button, when 'set' will open a rectangular magnification lens in the graph view</P>
<BLOCKQUOTE><BLOCKQUOTE>
<ul>
<li>MouseButton1 click-drag on the lens center circle to move the magnifier lens</li>
<li>MouseButton1 click-draw on a lens edge diamond to resize the magnifier lens </li>
<li>MouseButton1 click on the upper-right circle-cross to dispose of the magnifier lens</li>
<li>MouseWheel will change the magnification of the lens</li>
</ul>
<li>The <IMG src="images/filter_on.png"> button will open a Filter dialog. Select buttons in the dialog to hide specific vertices or edges in the display</li>
</BLOCKQUOTE></BLOCKQUOTE>
<P><A name="Show_Filters"/>The <IMG src="images/filter_on.png"> button will open a Filter dialog. Select buttons in the dialog to hide specific vertices or edges in the display.
The Filter dialog buttons are created by examining the graph vertex/edge properties to discover candidates for filtering.</P></BLOCKQUOTE></BLOCKQUOTE>
<P><A name="Arrangement"/>The <IMG src="images/katomic.png" width="16" height="16"> Arrangement menu is used to select one of several graph layout algorithms.</P>
<BLOCKQUOTE><BLOCKQUOTE>
<ul>
<li>The Filter dialog buttons are created by examining the graph vertex/edge properties to discover candidates for filtering</li>
</ul>
<li>Pull-Down the <IMG src="images/katomic.png" width="16" height="16"> Arrangement menu to select one of several graph layout algorithms.</li>
<li><A name="Compact_Hierarchical"/><B>Compact Hierarchical</B> is the <b>TidierTree Layout Algorithm</b>. It builds a tree structure and attempts to reduce horizontal space.</li>
<li><A name="Hierarchical"/><B>Hierarchical</B> is a basic Tree algorithm. It prioritizes 'important' edges while constructing the tree.</li>
<li><A name="Compact Radial"/><B>Compact Radial</B> is the <b>TidierTree Layout Algorithm</b> with the root(s) at the center and child vertices radiating outwards.</li>
<li><B>Hierarchical MinCross</B> is the <b>Sugiyama Layout Algorithm</b>. It attempts to route edges around vertices in order to reduce crossing.There are four layering algorithms:</li>
<ul>
<li>Compact Hierarchical is the <b>TidierTree Layout Algorithm</b>. It builds a tree structure and attempts to reduce horizontal space.</li>
<li>Hierarchical is a basic Tree algorithm. It prioritizes 'important' edges while constructing the tree.</li>
<li>Hierarchical - Edge-Aware is a basic Tree algorithm that attempts to align important edges vertically.</li>
<li>Hierarchical Multi Row is the Tree algorithm above, but it will create new rows (typewriter fashion) to reduce horizontal spread.</li>
<li>Compact Radial is the <b>TidierTree Layout Algorithm</b> with the root(s) at the center and child vertices radiating outwards.</li>
<li>Hierarchical MinCross is the <b>Sugiyama Layout Algorithm</b>. It attempts to route edges around vertices in order to reduce crossing.</li>
<ul>There are four layering algorithms:
<li>Top Down - biases the vertices to the top</li>
<li>Longest Path - biases the vertices to the bottom</li>
<li>Network Simplex - layers after finding an 'optimal tree'</li>
<li>Coffman Graham - biases the vertices using a scheduling algorithm to minimize length</li>
</ul>
<li>Circle will arrange vertices in a Circle. If there are not too many edges (less than specified in the jungrapht.circle.reduceEdgeCrossingMaxEdges property with a default of 200), it will attempt to reduce edge crossing by rearranging the vertices.</li>
<li>Force Balanced is a <b>Force Directed Layout Algorithm</b> using the the <b>Kamada Kawai</b> approach. It attempts to balance the graph by considering vertices and edge connections.</li>
<li>Force Directed is a <b>Force Directed Layout Algorithm</b> using the <b>Fructermann Reingold</b> approach. It pushes unconnected vertices apart and draws connected vertices together.</li>
<li>Radial is a Tree structure with the root(s) at the center and child vertices radiating outwards.</li>
<li>Balloon is a Tree structure with the root(s) at the centers of circles in a radial pattern</li>
<li>GEM is a Force Directed layout with locally separated components </li>
<li><A name="Hierarchical_MinCross_Top_Down"/><B>Top Down</B> - biases the vertices to the top</li>
<li><A name="Hierarchical_MinCross_Longest_Path"/><B>Longest Path</B> - biases the vertices to the bottom</li>
<li><A name="Hierarchical_MinCross_Network_Simplex"/><B>Network Simplex</B> - layers after finding an 'optimal tree'</li>
<li><A name="Hierarchical_MinCross_Coffman_Graham"/><B>Coffman Graham</B> - biases the vertices using a scheduling algorithm to minimize length</li>
</ul>
<li><A name="Circle"/><B>Circle</B> will arrange vertices in a Circle. If there are not too many edges (less than specified in the jungrapht.circle.reduceEdgeCrossingMaxEdges property with a default of 200), it will attempt to reduce edge crossing by rearranging the vertices.</li>
<li><A name="Force_Balanced"/><B>Force Balanced</B> is a <b>Force Directed Layout Algorithm</b> using the the <b>Kamada Kawai</b> approach. It attempts to balance the graph by considering vertices and edge connections.</li>
<li><A name="Force_Directed"/><B>Force Directed</B> is a <b>Force Directed Layout Algorithm</b> using the <b>Fructermann Reingold</b> approach. It pushes unconnected vertices apart and draws connected vertices together.</li>
<li><A name="Radial"/><B>Radial</B> is a Tree structure with the root(s) at the center and child vertices radiating outwards.</li>
<li><A name="Balloon"/><B>Balloon</B> is a Tree structure with the root(s) at the centers of circles in a radial pattern</li>
<li><A name="Gem__Graph_Embedder_"/><B>GEM</B> is a Force Directed layout with locally separated components </li>
</ul>
</BLOCKQUOTE></BLOCKQUOTE>
<H2>Popup Actions</H2>
<BLOCKQUOTE>
<H3> Standard Popup Actions</H3>
<ul>
<li><A name="Hide Selected"/><B>Hide Selected</B> - Causes the display to not show selected vertices. </li>
<li><A name="Hide Unselected"/><B>Hide Unselected</B> - Causes the display to not show unselected vertices.</li>
<li><A name="Invert Selection"/><B>Invert Selection</B> - Unselects all selected nodes and selects all unselected nodes.</li>
<li><A name="Grow Selection From Sources"/><B>Grow Selection From Sources</B> - Adds to the selection all vertices that have outgoing edges to the current selection.</li>
<li><A name="Grow Selection To Targets"/><B>Grow Selection To Targets</B> - Adds to the selection all vertices that have incoming edges from the current selection.</li>
<li><A name="Create Subgraph"/><B>Display Selected As New Graph</B> - Creates a new graph and display from the currently selected vertices.</li>
</ul>
<H3> Vertex Popup Actions</H3>
<ul>
<li><A name="Select Vertex"/><B>Select Vertex</B> - Selects the vertex that this action was invoked on.</li>
<li><A name="Deselect Vertex"/><B>Deselect Vertex</B> - Deselects the vertex that this action was invoked on.</li>
</ul>
<H3> Edge Popup Actions</H3>
<ul>
<li><A name="Edge Source"/><B>Go To Edge Source</B> - Makes this edge's source vertex be the focused vertex.</li>
<li><A name="Edge Target"/><B>Go To Edge Target</B> - Makes this edge's destination vertex be the focused vertex.</li>
<li><A name="Select Edge"/><B>Select Edge</B> - Add this edge and its associated vertices to the selection</li>
<li><A name="Deselect Edge"/><B>Deselect Edge</B> - Removes this edge and its associated vertices from the selection </li>
</ul>
</BLOCKQUOTE>
</BODY>
</HTML>

View file

@ -15,10 +15,11 @@
*/
package ghidra.graph.export;
import java.util.List;
import java.util.*;
import org.jgrapht.Graph;
import docking.action.DockingAction;
import docking.widgets.EventTrigger;
import ghidra.framework.plugintool.PluginTool;
import ghidra.service.graph.*;
@ -116,4 +117,19 @@ class ExportAttributedGraphDisplay implements GraphDisplay {
return description;
}
@Override
public void addAction(DockingAction action) {
// do nothing, actions are not supported by this display
}
@Override
public String getFocusedVertexId() {
return null;
}
@Override
public Set<String> getSelectedVertexIds() {
return Collections.emptySet();
}
}

View file

@ -24,6 +24,7 @@ import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@ -32,6 +33,7 @@ import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import org.jgrapht.Graph;
import org.jgrapht.graph.AsSubgraph;
import org.jungrapht.visualization.*;
import org.jungrapht.visualization.annotations.MultiSelectedVertexPaintable;
import org.jungrapht.visualization.annotations.SingleSelectedVertexPaintable;
@ -51,17 +53,16 @@ import org.jungrapht.visualization.transform.shape.MagnifyShapeTransformer;
import org.jungrapht.visualization.util.RectangleUtils;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.ToggleDockingAction;
import docking.action.builder.*;
import docking.menu.ActionState;
import docking.widgets.EventTrigger;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.graph.AttributeFilters;
import ghidra.graph.job.GraphJobRunner;
import ghidra.service.graph.*;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import resources.Icons;
@ -102,9 +103,9 @@ public class DefaultGraphDisplay implements GraphDisplay {
private final PluginTool pluginTool;
/**
* the {@link Plugin} that manages this {@link GraphDisplay}
* the "owner name" for action - mainly affects default help location
*/
private final String pluginName = "ProgramGraphPlugin";
private final String actionOwnerName = "GraphServices";
/**
* provides the component for the {@link GraphDisplay}
@ -161,12 +162,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
*/
private boolean freeFormSelection;
/**
* Handles the popup
*/
private GhidraGraphMouse graphMouse;
/**
* Will accept a {@link Graph} and use it to create a new graph display in
* a new tab or new window
*/
Consumer<Graph<AttributedVertex, AttributedEdge>> subgraphConsumer =
g -> {
Consumer<Graph<AttributedVertex, AttributedEdge>> subgraphConsumer = g -> {
try {
AttributedGraph attributedGraph = new AttributedGraph();
g.vertexSet().forEach(attributedGraph::addVertex);
@ -181,6 +186,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
// noop
}
};
private ToggleDockingAction hideSelectedAction;
private ToggleDockingAction hideUnselectedAction;
private SwitchableSelectionItemListener switchableSelectionListener;
/**
@ -217,7 +224,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
.builder(viewer.getRenderContext().getVertexBoundsFunction())
.build());
createActions();
graphMouse = new GhidraGraphMouse(componentProvider, viewer);
createToolbarActions();
createPopupActions();
connectSelectionStateListeners();
}
@ -287,10 +297,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
/**
* create the action icon buttons on the upper-right of the graph display window
*/
private void createActions() {
private void createToolbarActions() {
// create a toggle for 'scroll to selected vertex'
new ToggleActionBuilder("Scroll To Selection", pluginName)
new ToggleActionBuilder("Scroll To Selection", actionOwnerName)
.toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON)
.description("Ensure that the 'focused' vertex is visible")
.selected(true)
@ -302,7 +312,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
// create a toggle for enabling 'free-form' selection: selection is
// inside of a traced shape instead of a rectangle
new ToggleActionBuilder("Free-Form Selection", pluginName)
new ToggleActionBuilder("Free-Form Selection", actionOwnerName)
.toolBarIcon(DefaultDisplayGraphIcons.LASSO_ICON)
.description("Trace Free-Form Shape to select multiple vertices (CTRL-click-drag)")
.selected(false)
@ -311,13 +321,13 @@ public class DefaultGraphDisplay implements GraphDisplay {
.buildAndInstallLocal(componentProvider);
// create an icon button to display the satellite view
new ToggleActionBuilder("SatelliteView", pluginName).description("Show Satellite View")
new ToggleActionBuilder("SatelliteView", actionOwnerName).description("Show Satellite View")
.toolBarIcon(DefaultDisplayGraphIcons.SATELLITE_VIEW_ICON)
.onAction(this::toggleSatellite)
.buildAndInstallLocal(componentProvider);
// create an icon button to reset the view transformations to identity (scaled to layout)
new ActionBuilder("Reset View", pluginName)
new ActionBuilder("Reset View", actionOwnerName)
.description("Reset all view transforms to center graph in display")
.toolBarIcon(Icons.REFRESH_ICON)
.onAction(context -> viewer.scaleToLayout())
@ -325,7 +335,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
// create a button to show the view magnify lens
LensSupport<LensGraphMouse> magnifyViewSupport = createMagnifier();
ToggleDockingAction lensToggle = new ToggleActionBuilder("View Magnifier", pluginName)
ToggleDockingAction lensToggle = new ToggleActionBuilder("View Magnifier", actionOwnerName)
.description("Show View Magnifier")
.toolBarIcon(DefaultDisplayGraphIcons.VIEW_MAGNIFIER_ICON)
.onAction(context -> magnifyViewSupport.activate(
@ -336,13 +346,13 @@ public class DefaultGraphDisplay implements GraphDisplay {
componentProvider.addLocalAction(lensToggle);
// create an action button to show a dialog with generated filters
new ActionBuilder("Show Filters", pluginName).description("Show Graph Filters")
new ActionBuilder("Show Filters", actionOwnerName).description("Show Graph Filters")
.toolBarIcon(DefaultDisplayGraphIcons.FILTER_ICON)
.onAction(context -> showFilterDialog())
.buildAndInstallLocal(componentProvider);
// create a menu with graph layout algorithm selections
new MultiStateActionBuilder<String>("Arrangement", pluginName)
new MultiStateActionBuilder<String>("Arrangement", actionOwnerName)
.description("Select Layout Arrangement Algorithm")
.toolBarIcon(DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON)
.fireFirstAction(false)
@ -365,6 +375,179 @@ public class DefaultGraphDisplay implements GraphDisplay {
});
}
private void createPopupActions() {
new ActionBuilder("Select Vertex", actionOwnerName)
.popupMenuPath("Select Vertex")
.popupMenuGroup("selection", "1")
.withContext(VertexGraphActionContext.class)
.enabledWhen(c -> !viewer.getSelectedVertexState().isSelected(c.getClickedVertex()))
.onAction(c -> viewer.getSelectedVertexState().select(c.getClickedVertex()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Deselect Vertex", actionOwnerName)
.popupMenuPath("Deselect Vertex")
.popupMenuGroup("selection", "2")
.withContext(VertexGraphActionContext.class)
.enabledWhen(c -> viewer.getSelectedVertexState().isSelected(c.getClickedVertex()))
.onAction(c -> viewer.getSelectedVertexState().deselect(c.getClickedVertex()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Select Edge", actionOwnerName)
.popupMenuPath("Select Edge")
.popupMenuGroup("selection", "1")
.withContext(EdgeGraphActionContext.class)
.enabledWhen(c -> !viewer.getSelectedEdgeState().isSelected(c.getClickedEdge()))
.onAction(c -> selectEdge(c.getClickedEdge()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Deselect Edge", actionOwnerName)
.popupMenuPath("Deselect Edge")
.popupMenuGroup("selection", "2")
.withContext(EdgeGraphActionContext.class)
.enabledWhen(c -> viewer.getSelectedEdgeState().isSelected(c.getClickedEdge()))
.onAction(c -> deselectEdge(c.getClickedEdge()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Edge Source", actionOwnerName)
.popupMenuPath("Go To Edge Source")
.popupMenuGroup("Go To")
.withContext(EdgeGraphActionContext.class)
.onAction(c -> setFocusedVertex(graph.getEdgeSource(c.getClickedEdge())))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Edge Target", actionOwnerName)
.popupMenuPath("Go To Edge Target")
.popupMenuGroup("Go To")
.withContext(EdgeGraphActionContext.class)
.onAction(c -> setFocusedVertex(graph.getEdgeTarget(c.getClickedEdge())))
.buildAndInstallLocal(componentProvider);
hideSelectedAction = new ToggleActionBuilder("Hide Selected", actionOwnerName)
.popupMenuPath("Hide Selected")
.popupMenuGroup("z", "1")
.description("Toggles whether or not to show selected vertices and edges")
.onAction(c -> manageVertexDisplay())
.buildAndInstallLocal(componentProvider);
hideUnselectedAction = new ToggleActionBuilder("Hide Unselected", actionOwnerName)
.popupMenuPath("Hide Unselected")
.popupMenuGroup("z", "2")
.description("Toggles whether or not to show selected vertices and edges")
.onAction(c -> manageVertexDisplay())
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Invert Selection", actionOwnerName)
.popupMenuPath("Invert Selection")
.popupMenuGroup("z", "3")
.description("Inverts the current selection")
.onAction(c -> invertSelection())
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Grow Selection To Targets", actionOwnerName)
.popupMenuPath("Grow Selection To Targets")
.popupMenuGroup("z", "4")
.description("Extends the current selection by including the target vertex " +
"of all edges whose source is selected")
.keyBinding("ctrl O")
.enabledWhen(c -> !isAllSelected(getTargetVerticesFromSelected()))
.onAction(c -> growSelection(getTargetVerticesFromSelected()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Grow Selection From Sources", actionOwnerName)
.popupMenuPath("Grow Selection From Sources")
.popupMenuGroup("z", "4")
.description("Extends the current selection by including the target vertex " +
"of all edges whose source is selected")
.keyBinding("ctrl I")
.enabledWhen(c -> !isAllSelected(getSourceVerticesFromSelected()))
.onAction(c -> growSelection(getSourceVerticesFromSelected()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Create Subgraph", actionOwnerName)
.popupMenuPath("Display Selected as New Graph")
.popupMenuGroup("z", "5")
.description("Creates a subgraph from the selected nodes")
.enabledWhen(c -> !viewer.getSelectedVertexState().getSelected().isEmpty())
.onAction(c -> createAndDisplaySubGraph())
.buildAndInstallLocal(componentProvider);
}
private void createAndDisplaySubGraph() {
GraphDisplay display = graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
try {
display.setGraph(createSubGraph(), "SubGraph", false, TaskMonitor.DUMMY);
display.setGraphDisplayListener(listener.cloneWith(display));
}
catch (CancelledException e) {
// using Dummy, so can't happen
}
}
private AttributedGraph createSubGraph() {
Set<AttributedVertex> selected = viewer.getSelectedVertexState().getSelected();
Graph<AttributedVertex, AttributedEdge> subGraph = new AsSubgraph<>(graph, selected);
AttributedGraph newGraph = new AttributedGraph();
subGraph.vertexSet().forEach(newGraph::addVertex);
subGraph.edgeSet().forEach(e -> {
AttributedVertex source = subGraph.getEdgeSource(e);
AttributedVertex target = subGraph.getEdgeTarget(e);
newGraph.addEdge(source, target, e);
});
return newGraph;
}
private void growSelection(Set<AttributedVertex> vertices) {
viewer.getSelectedVertexState().select(vertices);
}
private boolean isAllSelected(Set<AttributedVertex> vertices) {
return viewer.getSelectedVertexState().getSelected().containsAll(vertices);
}
private Set<AttributedVertex> getTargetVerticesFromSelected() {
Set<AttributedVertex> targets = new HashSet<>();
Set<AttributedVertex> selectedVertices = getSelectedVertices();
selectedVertices.forEach(v -> {
Set<AttributedEdge> edges = graph.outgoingEdgesOf(v);
edges.forEach(e -> targets.add(graph.getEdgeTarget(e)));
});
return targets;
}
private Set<AttributedVertex> getSourceVerticesFromSelected() {
Set<AttributedVertex> sources = new HashSet<>();
Set<AttributedVertex> selectedVertices = getSelectedVertices();
selectedVertices.forEach(v -> {
Set<AttributedEdge> edges = graph.incomingEdgesOf(v);
edges.forEach(e -> sources.add(graph.getEdgeSource(e)));
});
return sources;
}
private void invertSelection() {
switchableSelectionListener.setEnabled(false);
try {
MutableSelectedState<AttributedVertex> selectedVertexState =
viewer.getSelectedVertexState();
graph.vertexSet().forEach(v -> {
if (selectedVertexState.isSelected(v)) {
selectedVertexState.deselect(v);
}
else {
selectedVertexState.select(v);
}
});
Set<AttributedVertex> selected = selectedVertexState.getSelected();
List<String> selectedIds =
selected.stream().map(AttributedVertex::getId).collect(Collectors.toList());
notifySelectionChanged(selectedIds);
}
finally {
switchableSelectionListener.setEnabled(true);
}
}
/**
* get a {@code List} of {@code ActionState} buttons for the
* configured layout algorithms
@ -374,8 +557,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
String[] names = layoutTransitionManager.getLayoutNames();
List<ActionState<String>> actionStates = new ArrayList<>();
for (String layoutName : names) {
actionStates.add(new ActionState<>(layoutName,
DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON, layoutName));
ActionState<String> state = new ActionState<>(layoutName,
DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON, layoutName);
state.setHelpLocation(new HelpLocation(actionOwnerName, layoutName));
actionStates.add(state);
}
return actionStates;
}
@ -489,6 +674,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
listener.graphClosed();
}
listener = null;
componentProvider.closeComponent();
}
/**
@ -501,18 +687,23 @@ public class DefaultGraphDisplay implements GraphDisplay {
this.listener.graphClosed();
}
this.listener = listener;
DefaultGraphMouse<AttributedVertex, AttributedEdge> graphMouse =
GhidraGraphMouse.<AttributedVertex, AttributedEdge> builder()
.viewer(viewer)
.subgraphConsumer(subgraphConsumer)
.locatedVertexConsumer(this::setFocusedVertex)
.graphDisplayListener(listener)
.vertexIdFunction(AttributedVertex::getId)
.vertexNameFunction(AttributedVertex::getName)
.build();
viewer.setGraphMouse(graphMouse);
}
private void deselectEdge(AttributedEdge edge) {
viewer.getSelectedEdgeState().deselect(edge);
AttributedVertex source = graph.getEdgeSource(edge);
AttributedVertex target = graph.getEdgeTarget(edge);
viewer.getSelectedVertexState().deselect(Set.of(source, target));
}
private void selectEdge(AttributedEdge edge) {
viewer.getSelectedEdgeState().select(edge);
AttributedVertex source = graph.getEdgeSource(edge);
AttributedVertex target = graph.getEdgeTarget(edge);
viewer.getSelectedVertexState().select(Set.of(source, target));
}
/**
* connect the selection state to to the visualization
*/
@ -1006,6 +1197,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
vv.getComponent().requestFocus();
vv.setBackground(Color.WHITE);
MouseListener[] mouseListeners = vv.getComponent().getMouseListeners();
for (MouseListener mouseListener : mouseListeners) {
vv.getComponent().removeMouseListener(mouseListener);
}
return vv;
}
@ -1051,6 +1247,78 @@ public class DefaultGraphDisplay implements GraphDisplay {
void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
@Override
public void addAction(DockingAction action) {
componentProvider.addLocalAction(action);
}
@Override
public String getFocusedVertexId() {
return focusedVertex == null ? null : focusedVertex.getId();
}
@Override
public Set<String> getSelectedVertexIds() {
Set<AttributedVertex> selectedVertices = getSelectedVertices();
return selectedVertices.stream().map(v -> v.getId()).collect(Collectors.toSet());
}
public ActionContext getActionContext(MouseEvent e) {
AttributedVertex pickedVertex = graphMouse.getPickedVertex(e);
if (pickedVertex != null) {
return new VertexGraphActionContext(componentProvider, graph, getSelectedVertices(),
focusedVertex, pickedVertex);
}
AttributedEdge pickedEdge = graphMouse.getPickedEdge(e);
if (pickedEdge != null) {
return new EdgeGraphActionContext(componentProvider, graph, getSelectedVertices(),
focusedVertex, pickedEdge);
}
return new GraphActionContext(componentProvider, graph, getSelectedVertices(),
focusedVertex);
}
private Set<AttributedVertex> getSelectedVertices() {
return viewer.getSelectedVertexState().getSelected();
}
/**
* Use the hide selected action states to determine what vertices are shown:
* <ul>
* <li>unselected vertices only</li>
* <li>selected vertices only</li>
* <li>both selected and unselected vertices are shown</li>
* <li>neither selected nor unselected vertices are shown</li>
* </ul>
*/
private void manageVertexDisplay() {
boolean hideSelected = hideSelectedAction.isSelected();
boolean hideUnselected = hideUnselectedAction.isSelected();
MutableSelectedState<AttributedVertex> selectedVertexState =
viewer.getSelectedVertexState();
if (hideSelected && hideUnselected) {
viewer.getRenderContext()
.setVertexIncludePredicate(v -> false);
}
else if (hideSelected) {
viewer.getRenderContext()
.setVertexIncludePredicate(Predicate.not(selectedVertexState::isSelected));
}
else if (hideUnselected) {
viewer.getRenderContext()
.setVertexIncludePredicate(selectedVertexState::isSelected);
}
else {
viewer.getRenderContext()
.setVertexIncludePredicate(v -> true);
}
viewer.repaint();
}
}

View file

@ -15,10 +15,14 @@
*/
package ghidra.graph.visualization;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import docking.ActionContext;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.PluginTool;
import ghidra.service.graph.GraphDisplay;
import ghidra.util.HelpLocation;
/**
@ -28,7 +32,7 @@ public class DefaultGraphDisplayComponentProvider extends ComponentProviderAdapt
static final String WINDOW_GROUP = "ProgramGraph";
private static final String WINDOW_MENU_GROUP_NAME = "Graph";
private final DefaultGraphDisplay display;
private DefaultGraphDisplay display;
DefaultGraphDisplayComponentProvider(DefaultGraphDisplay display, PluginTool pluginTool) {
super(pluginTool, "Graph", "DefaultGraphDisplay");
@ -52,7 +56,17 @@ public class DefaultGraphDisplayComponentProvider extends ComponentProviderAdapt
@Override
public void closeComponent() {
if (display != null) {
super.closeComponent();
display.close();
// to prevent looping, null out display before callings its close method.
GraphDisplay closingDisplay = display;
display = null;
closingDisplay.close();
}
}
@Override
public ActionContext getActionContext(MouseEvent event) {
return display.getActionContext(event);
}
}

View file

@ -15,148 +15,94 @@
*/
package ghidra.graph.visualization;
import ghidra.service.graph.GraphDisplayListener;
import org.jgrapht.Graph;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.control.AbstractPopupGraphMousePlugin;
import org.jungrapht.visualization.control.DefaultGraphMouse;
import org.jungrapht.visualization.control.GraphElementAccessor;
import org.jungrapht.visualization.control.TransformSupport;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.selection.ShapePickSupport;
import static org.jungrapht.visualization.VisualizationServer.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jungrapht.visualization.VisualizationServer.PREFIX;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.control.*;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.selection.ShapePickSupport;
import docking.ComponentProvider;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
/**
* An extension of the jungrapht DefaultGraphMouse. This class has references to
* <ul>
* <li>a {@link VisualizationViewer} (to access the Graph and LayoutModel)
* <li>a {@link Consumer} of the Subgraph (to make new Graph displays)
* <li>a {@link GraphDisplayListener} (to react to changes in node attributes)
* <li>a {@code Function} to supply the id for a given vertex
* <li>a {@code Function} to supply the name for a given vertex
*
*/
public class GhidraGraphMouse<V, E> extends DefaultGraphMouse<V, E> {
public class GhidraGraphMouse extends DefaultGraphMouse<AttributedVertex, AttributedEdge> {
private static final String PICK_AREA_SIZE = PREFIX + "pickAreaSize";
private static final String PICK_AREA_SIZE_PROPERTY = PREFIX + "pickAreaSize";
/**
* holds the context for graph visualization
*/
VisualizationViewer<V, E> viewer;
/**
* will accept a {@link Graph} and display it in a new tab or window
*/
Consumer<Graph<V, E>> subgraphConsumer;
private VisualizationViewer<AttributedVertex, AttributedEdge> viewer;
/**
* Accepts a vertex that was 'located'
*/
Consumer<V> locatedVertexConsumer;
/**
* a listener for events, notably the event to request change of a vertex name
*/
GraphDisplayListener graphDisplayListener;
/**
* supplies the id for a given vertex
*/
Function<V, String> vertexIdFunction;
/**
* supplies the name for a given vertex
*/
Function<V, String> vertexNameFunction;
/**
* create an instance
* @param <V> vertex type
* @param <E> edge type
* @return a configured GhidraGraphMouseBuilder
*/
public static <V, E> GhidraGraphMouseBuilder<V, E, ?, ?> builder() {
return new GhidraGraphMouseBuilder<>();
}
private int pickSize;
/**
* create an instance with default values
* @param componentProvider the graph component provider
* @param viewer the graph viewer component
*/
GhidraGraphMouse(GhidraGraphMouseBuilder<V, E, ?, ?> builder) {
super(builder.vertexSelectionOnly(true));
this.viewer = builder.viewer;
this.subgraphConsumer = builder.subgraphConsumer;
this.locatedVertexConsumer = builder.locatedVertexConsumer;
this.graphDisplayListener = builder.graphDisplayListener;
this.vertexIdFunction = builder.vertexIdFunction;
this.vertexNameFunction = builder.vertexNameFunction;
GhidraGraphMouse(ComponentProvider componentProvider,
VisualizationViewer<AttributedVertex, AttributedEdge> viewer) {
super(DefaultGraphMouse.<AttributedVertex, AttributedEdge> builder());
this.viewer = viewer;
pickSize = Integer.getInteger(GhidraGraphMouse.PICK_AREA_SIZE_PROPERTY, 4);
}
/**
* create the plugins, and load them
*/
@Override
public void loadPlugins() {
add(new PopupPlugin());
super.loadPlugins();
}
class PopupPlugin extends AbstractPopupGraphMousePlugin {
SelectionFilterMenu<V, E> selectionFilterMenu;
PopupPlugin() {
this.selectionFilterMenu = new SelectionFilterMenu<>(viewer, subgraphConsumer);
}
@Override
protected void handlePopup(MouseEvent e) {
int pickSize = Integer.getInteger(PICK_AREA_SIZE, 4);
Rectangle2D footprintRectangle =
new Rectangle2D.Float(
(float) e.getPoint().x - pickSize / 2f,
(float) e.getPoint().y - pickSize / 2f,
private Rectangle2D getFootprint(MouseEvent e) {
return new Rectangle2D.Float(
e.getPoint().x - pickSize / 2f,
e.getPoint().y - pickSize / 2f,
pickSize,
pickSize);
}
LayoutModel<V> layoutModel = viewer.getVisualizationModel().getLayoutModel();
GraphElementAccessor<V, E> pickSupport = viewer.getPickSupport();
V pickedVertex;
E pickedEdge = null;
AttributedEdge getPickedEdge(MouseEvent e) {
if (e == null) {
return null;
}
Rectangle2D footprintRectangle = getFootprint(e);
LayoutModel<AttributedVertex> layoutModel = viewer.getVisualizationModel().getLayoutModel();
GraphElementAccessor<AttributedVertex, AttributedEdge> pickSupport =
viewer.getPickSupport();
if (pickSupport instanceof ShapePickSupport) {
ShapePickSupport<V, E> shapePickSupport =
(ShapePickSupport<V, E>) pickSupport;
pickedVertex = shapePickSupport.getVertex(layoutModel, footprintRectangle);
if (pickedVertex == null) {
pickedEdge = shapePickSupport.getEdge(layoutModel, footprintRectangle);
ShapePickSupport<AttributedVertex, AttributedEdge> shapePickSupport =
(ShapePickSupport<AttributedVertex, AttributedEdge>) pickSupport;
return shapePickSupport.getEdge(layoutModel, footprintRectangle);
}
} else {
TransformSupport<V, E> transformSupport = viewer.getTransformSupport();
TransformSupport<AttributedVertex, AttributedEdge> transformSupport =
viewer.getTransformSupport();
Point2D layoutPoint = transformSupport.inverseTransform(viewer, e.getPoint());
pickedVertex = pickSupport.getVertex(layoutModel, layoutPoint.getX(), layoutPoint.getY());
if (pickedVertex == null) {
pickedEdge = pickSupport.getEdge(layoutModel, layoutPoint.getX(), layoutPoint.getY());
}
}
if (pickedVertex != null) {
OnVertexSelectionMenu<V, E> menu =
new OnVertexSelectionMenu<>(viewer, graphDisplayListener,
vertexIdFunction, vertexNameFunction,
pickedVertex);
menu.show(viewer.getComponent(), e.getX(), e.getY());
} else if (pickedEdge != null) {
OnEdgeSelectionMenu<V, E> menu =
new OnEdgeSelectionMenu<>(viewer, locatedVertexConsumer, pickedEdge);
menu.show(viewer.getComponent(), e.getX(), e.getY());
} else {
selectionFilterMenu.show(viewer.getComponent(), e.getX(), e.getY());
}
}
}
return pickSupport.getEdge(layoutModel, layoutPoint.getX(), layoutPoint.getY());
}
AttributedVertex getPickedVertex(MouseEvent e) {
if (e == null) {
return null;
}
Rectangle2D footprintRectangle = getFootprint(e);
LayoutModel<AttributedVertex> layoutModel = viewer.getVisualizationModel().getLayoutModel();
GraphElementAccessor<AttributedVertex, AttributedEdge> pickSupport =
viewer.getPickSupport();
if (pickSupport instanceof ShapePickSupport) {
ShapePickSupport<AttributedVertex, AttributedEdge> shapePickSupport =
(ShapePickSupport<AttributedVertex, AttributedEdge>) pickSupport;
return shapePickSupport.getVertex(layoutModel, footprintRectangle);
}
TransformSupport<AttributedVertex, AttributedEdge> transformSupport =
viewer.getTransformSupport();
Point2D layoutPoint = transformSupport.inverseTransform(viewer, e.getPoint());
return pickSupport.getVertex(layoutModel, layoutPoint.getX(), layoutPoint.getY());
}
}

View file

@ -1,116 +0,0 @@
/* ###
* 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.graph.visualization;
import ghidra.service.graph.GraphDisplayListener;
import org.jgrapht.Graph;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.control.DefaultGraphMouse;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jungrapht.visualization.VisualizationServer.PREFIX;
/**
* An extension of the jungrapht DefaultGraphMouse.Builder. This class has references to
* <ul>
* <li>a {@link VisualizationViewer} (to access the Graph and LayoutModel)
* <li>a {@link Consumer} of the Subgraph (to make new Graph displays)
* <li>a {@link GraphDisplayListener} (to react to changes in node attributes)
* <li>a {@code Function} to supply the id for a given vertex
* <li>a {@code Function} to supply the name for a given vertex
*
*/
public class GhidraGraphMouseBuilder<V, E, T extends GhidraGraphMouse<V, E>, B extends GhidraGraphMouseBuilder<V, E, T, B>>
extends DefaultGraphMouse.Builder<V, E, T, B> {
private static final String PICK_AREA_SIZE = PREFIX + "pickAreaSize";
/**
* holds the context for graph visualization
*/
VisualizationViewer<V, E> viewer;
/**
* will accept a {@link Graph} and display it in a new tab or window
*/
Consumer<Graph<V, E>> subgraphConsumer;
/**
* accepts a 'located' vertex via a menu driven action
*/
Consumer<V> locatedVertexConsumer;
/**
* a listener for events, notably the event to request change of a vertex name
*/
GraphDisplayListener graphDisplayListener;
/**
* supplies the id for a given vertex
*/
Function<V, String> vertexIdFunction;
/**
* supplies the name for a given vertex
*/
Function<V, String> vertexNameFunction;
public B self() {
return (B) this;
}
public B viewer(VisualizationViewer<V, E> viewer) {
this.viewer = viewer;
return self();
}
public B subgraphConsumer(Consumer<Graph<V, E>> subgraphConsumer) {
this.subgraphConsumer = subgraphConsumer;
return self();
}
public B locatedVertexConsumer(Consumer<V> locatedVertexConsumer) {
this.locatedVertexConsumer = locatedVertexConsumer;
return self();
}
public B graphDisplayListener(GraphDisplayListener graphDisplayListener) {
this.graphDisplayListener = graphDisplayListener;
return self();
}
public B vertexIdFunction(Function<V, String> vertexIdFunction) {
this.vertexIdFunction = vertexIdFunction;
return self();
}
public B vertexNameFunction(Function<V, String> vertexNameFunction) {
this.vertexNameFunction = vertexIdFunction;
return self();
}
public T build() {
return (T) new GhidraGraphMouse(this);
}
public static <V, E> GhidraGraphMouseBuilder<V, E, ?, ?> builder() {
return new GhidraGraphMouseBuilder<>();
}
}

View file

@ -1,64 +0,0 @@
/* ###
* 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.graph.visualization;
import org.jgrapht.Graph;
import org.jungrapht.visualization.VisualizationViewer;
import javax.swing.AbstractButton;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import java.util.Set;
import java.util.function.Consumer;
/**
* a Popup menu to allow actions relative to a particular edge.
* The popup appears on a right click over an edge in the display.
* The user can select/deselect the edge. When the edge is selected,
* its endpoints are also selected and the target endpoint is 'located'
*/
public class OnEdgeSelectionMenu<V, E> extends JPopupMenu {
public OnEdgeSelectionMenu(VisualizationViewer<V, E> visualizationViewer,
Consumer<V> locatedVertexConsumer,
E edge) {
Graph<V, E> graph = visualizationViewer.getVisualizationModel().getGraph();
V source = graph.getEdgeSource(edge);
V target = graph.getEdgeTarget(edge);
AbstractButton selectButton = new JMenuItem("Select Edge");
AbstractButton deselectButton = new JMenuItem("Deselect Edge");
selectButton.addActionListener(evt -> {
visualizationViewer.getSelectedEdgeState().select(edge);
visualizationViewer.getSelectedVertexState().select(Set.of(source, target));
locatedVertexConsumer.accept(target);
});
deselectButton.addActionListener(evt -> {
visualizationViewer.getSelectedEdgeState().deselect(edge);
visualizationViewer.getSelectedVertexState().deselect(Set.of(source, target));
});
add(visualizationViewer.getSelectedEdgeState().isSelected(edge) ? deselectButton : selectButton);
AbstractButton locateSourceButton = new JMenuItem("Locate Edge Source");
locateSourceButton.addActionListener(evt -> {
locatedVertexConsumer.accept(source);
});
AbstractButton locateTargetButton = new JMenuItem("Locate Edge Target");
locateTargetButton.addActionListener(evt -> {
locatedVertexConsumer.accept(target);
});
add(locateSourceButton);
add(locateTargetButton);
}
}

View file

@ -1,68 +0,0 @@
/* ###
* 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.graph.visualization;
import ghidra.service.graph.GraphDisplayListener;
import org.apache.commons.lang3.StringUtils;
import org.jungrapht.visualization.VisualizationViewer;
import javax.swing.AbstractButton;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import java.util.function.Function;
/**
* a Popup menu to allow actions relative to a particular vertex.
* The popup appears on a right click over a vertex in the display.
* The user can:
* <ul>
* <li>select/deselect the vertex
* <li>rename the selected vertex (may modify the value in the listing)
* <li>re-label the selected vertex locally (affects only the local visual display)
*/
public class OnVertexSelectionMenu<V, E> extends JPopupMenu {
public OnVertexSelectionMenu(VisualizationViewer<V, E> visualizationViewer,
GraphDisplayListener graphDisplayListener,
Function<V, String> vertexIdFunction,
Function<V, String> vertexNameFunction,
V vertex) {
AbstractButton selectButton = new JMenuItem("Select Vertex");
AbstractButton deselectButton = new JMenuItem("Deselect Vertex");
AbstractButton renameAttributeButton = new JMenuItem("Rename vertex");
renameAttributeButton.addActionListener(evt -> {
String newName = JOptionPane.showInputDialog("New Name Attribute");
if (!StringUtils.isEmpty(newName)) {
graphDisplayListener.updateVertexName(vertexIdFunction.apply(vertex),
vertexNameFunction.apply(vertex), newName);
}
}
);
selectButton.addActionListener(evt -> {
if (vertex != null) {
visualizationViewer.getSelectedVertexState().select(vertex);
}
});
deselectButton.addActionListener(evt -> {
if (vertex != null) {
visualizationViewer.getSelectedVertexState().deselect(vertex);
}
});
add(visualizationViewer.getSelectedVertexState().isSelected(vertex) ? deselectButton : selectButton);
add(renameAttributeButton);
}
}

View file

@ -113,7 +113,6 @@ public class BlockGraphTask extends Task {
private String actionName;
private Program program;
public BlockGraphTask(String actionName, boolean graphEntryPointNexus, boolean showCode,
boolean reuseGraph, boolean appendGraph, PluginTool tool, ProgramSelection selection,
ProgramLocation location, CodeBlockModel blockModel,
@ -210,7 +209,6 @@ public class BlockGraphTask extends Task {
return graph;
}
private CodeBlockIterator getBlockIterator() throws CancelledException {
if (selection == null || selection.isEmpty()) {
return blockModel.getCodeBlocks(taskMonitor);
@ -218,7 +216,8 @@ public class BlockGraphTask extends Task {
return blockModel.getCodeBlocksContaining(selection, taskMonitor);
}
private Address graphBlock(AttributedGraph graph, CodeBlock curBB, List<AttributedVertex> entries)
private Address graphBlock(AttributedGraph graph, CodeBlock curBB,
List<AttributedVertex> entries)
throws CancelledException {
Address[] startAddrs = curBB.getStartAddresses();
@ -255,7 +254,6 @@ public class BlockGraphTask extends Task {
}
}
protected AttributedVertex graphBasicBlock(AttributedGraph graph, CodeBlock curBB)
throws CancelledException {
@ -297,7 +295,8 @@ public class BlockGraphTask extends Task {
return fromVertex;
}
private void setEdgeColor(AttributedEdge edge, AttributedVertex fromVertex, AttributedVertex toVertex) {
private void setEdgeColor(AttributedEdge edge, AttributedVertex fromVertex,
AttributedVertex toVertex) {
// color the edge: first on the 'from' vertex, then try to 'to' vertex
String fromColor = fromVertex.getAttribute("Color");
String toColor = toVertex.getAttribute("Color");

View file

@ -17,14 +17,15 @@ package ghidra.graph.program;
import java.util.*;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.graph.AddressBasedGraphDisplayListener;
import ghidra.app.util.AddEditDialog;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.model.block.*;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolTable;
import ghidra.service.graph.GraphDisplay;
import ghidra.service.graph.GraphDisplayListener;
import ghidra.service.graph.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -39,6 +40,17 @@ public class BlockModelGraphDisplayListener extends AddressBasedGraphDisplayList
GraphDisplay display) {
super(tool, blockModel.getProgram(), display);
this.blockModel = blockModel;
addActions(display);
}
private void addActions(GraphDisplay display) {
display.addAction(new ActionBuilder("Rename Vertex", "Block Graph")
.popupMenuPath("Rename Vertex")
.withContext(VertexGraphActionContext.class)
// only enable action when vertex corresponds to an address
.enabledWhen(c -> getAddress(c.getClickedVertex().getId()) != null)
.onAction(this::updateVertexName)
.build());
}
@Override
@ -137,4 +149,24 @@ public class BlockModelGraphDisplayListener extends AddressBasedGraphDisplayList
return program.getMemory().contains(addr) || addr.isExternalAddress();
}
private void updateVertexName(VertexGraphActionContext context) {
String vertexId = context.getClickedVertex().getId();
Address address = getAddressForVertexId(vertexId);
Symbol symbol = program.getSymbolTable().getPrimarySymbol(address);
if (symbol == null) {
AddEditDialog dialog = new AddEditDialog("Create Label", tool);
dialog.addLabel(address, program, context.getComponentProvider());
}
else {
AddEditDialog dialog = new AddEditDialog("Edit Label", tool);
dialog.editLabel(symbol, program, context.getComponentProvider());
}
}
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new BlockModelGraphDisplayListener(tool, blockModel, graphDisplay);
}
}

View file

@ -17,6 +17,7 @@ package ghidra.graph.program;
import java.util.*;
import docking.action.DockingAction;
import docking.widgets.EventTrigger;
import ghidra.service.graph.*;
import ghidra.util.exception.CancelledException;
@ -110,4 +111,20 @@ public class TestGraphDisplay implements GraphDisplay {
public void selectionChanged(List<String> vertexIds) {
listener.selectionChanged(vertexIds);
}
@Override
public void addAction(DockingAction action) {
// do nothing, actions are not supported by this display
}
@Override
public String getFocusedVertexId() {
return currentFocusedVertex;
}
@Override
public Set<String> getSelectedVertexIds() {
return new HashSet<String>(currentSelection);
}
}

View file

@ -91,7 +91,8 @@ public class PopupActionManager implements PropertyChangeListener {
popupMenu.show(c, e.getX(), e.getY());
}
JPopupMenu createPopupMenu(Iterator<DockingActionIf> localActions, ActionContext context) {
protected JPopupMenu createPopupMenu(Iterator<DockingActionIf> localActions,
ActionContext context) {
if (localActions == null) {
localActions = IteratorUtils.emptyIterator();

View file

@ -34,4 +34,9 @@ public class DummyGraphDisplayListener implements GraphDisplayListener {
// I'm a dummy
}
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new DummyGraphDisplayListener();
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.service.graph;
import java.util.Set;
import docking.ComponentProvider;
/**
* GraphActionContext for when user invokes a popup action on a graph edge.
*/
public class EdgeGraphActionContext extends GraphActionContext {
private AttributedEdge clickedEdge;
public EdgeGraphActionContext(ComponentProvider componentProvider,
AttributedGraph graph, Set<AttributedVertex> selectedVertices,
AttributedVertex locatedVertex, AttributedEdge clickedEdge) {
super(componentProvider, graph, selectedVertices, locatedVertex);
this.clickedEdge = clickedEdge;
}
/**
* Returns the edge from where the popup menu was launched
* @return the edge from where the popup menu was launched
*/
public AttributedEdge getClickedEdge() {
return clickedEdge;
}
}

View file

@ -0,0 +1,65 @@
/* ###
* 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.service.graph;
import java.util.Set;
import docking.ActionContext;
import docking.ComponentProvider;
/**
* The base ActionContext for the GraphDisplay instances.
*/
public class GraphActionContext extends ActionContext {
private final AttributedGraph graph;
private final Set<AttributedVertex> selectedVertices;
private final AttributedVertex focusedVertex;
public GraphActionContext(ComponentProvider componentProvider,
AttributedGraph graph, Set<AttributedVertex> selectedVertices,
AttributedVertex locatedVertex) {
super(componentProvider);
this.graph = graph;
this.selectedVertices = selectedVertices;
this.focusedVertex = locatedVertex;
}
/**
* Returns the graph
* @return the graph
*/
public AttributedGraph getGraph() {
return graph;
}
/**
* Returns the set of selectedVertices in the graph
* @return the set of selectedVertices in the graph
*/
public Set<AttributedVertex> getSelectedVertices() {
return selectedVertices;
}
/**
* Returns the focused vertex (similar concept to the cursor in a text document)
* @return the focused vertex
*/
public AttributedVertex getFocusedVertex() {
return focusedVertex;
}
}

View file

@ -16,7 +16,9 @@
package ghidra.service.graph;
import java.util.List;
import java.util.Set;
import docking.action.DockingAction;
import docking.widgets.EventTrigger;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -55,6 +57,12 @@ public interface GraphDisplay {
*/
public void setLocationFocus(String vertexID, EventTrigger eventTrigger);
/**
* Returns the currently focused vertexID or null if no vertex is focussed.
* @return the currently focused vertexID or null if no vertex is focussed.
*/
public String getFocusedVertexId();
/**
* Tells the graph display window to select the vertices with the given ids
*
@ -68,6 +76,12 @@ public interface GraphDisplay {
*/
public void selectVertices(List<String> vertexList, EventTrigger eventTrigger);
/**
* Returns a list of vertex ids for all the currently selected vertices
* @return a list of vertex ids for all the currently selected vertices
*/
public Set<String> getSelectedVertexIds();
/**
* Closes this graph display window.
*/
@ -127,4 +141,11 @@ public interface GraphDisplay {
* @return the description of the current graph
*/
public String getGraphDescription();
/**
* Adds the action to the graph display. Not all GraphDisplays support adding custom
* actions, so this may have no effect.
* @param action the action to add.
*/
public void addAction(DockingAction action);
}

View file

@ -18,7 +18,7 @@ package ghidra.service.graph;
import java.util.List;
/**
* Interface for being notified when the user interacts with a visual graph display.
* Interface for being notified when the user interacts with a visual graph display
*/
public interface GraphDisplayListener {
/**
@ -29,18 +29,24 @@ public interface GraphDisplayListener {
/**
* Notification that the list of selected vertices has changed
*
* @param vertexIds the list of vertex ids for the currently selected vertices.
* @param vertexIds the list of vertex ids for the currently selected vertices
*/
public void selectionChanged(List<String> vertexIds);
/**
* Notification that the "focused" (active) vertex has changed.
* Notification that the "focused" (active) vertex has changed
* @param vertexId the vertex id of the currently "focused" vertex
*/
public void locationFocusChanged(String vertexId);
default boolean updateVertexName(String vertexId, String oldName, String newName) {
// no op
return false;
}
/**
* Makes a new GraphDisplayListener of the same type as the specific
* instance of this GraphDisplayListener
*
* @param graphDisplay the new {@link GraphDisplay} the new listener will support
* @return A new instance of a GraphDisplayListener that is the same type as as the instance
* on which it is called
*/
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay);
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.service.graph;
import java.util.Set;
import docking.ComponentProvider;
/**
* GraphActionContext for when user invokes a popup action on a graph vertex.
*/
public class VertexGraphActionContext extends GraphActionContext {
private AttributedVertex clickedVertex;
public VertexGraphActionContext(ComponentProvider componentProvider,
AttributedGraph graph, Set<AttributedVertex> selectedVertices,
AttributedVertex locatedVertex, AttributedVertex clickedVertex) {
super(componentProvider, graph, selectedVertices, locatedVertex);
this.clickedVertex = clickedVertex;
}
/**
* Returns the vertex from where the popup menu was launched
* @return the vertex from where the popup menu was launched
*/
public AttributedVertex getClickedVertex() {
return clickedVertex;
}
}

View file

@ -0,0 +1,313 @@
/* ###
* 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.graph;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;
import docking.ComponentProvider;
import docking.action.DockingActionIf;
import docking.widgets.EventTrigger;
import ghidra.app.plugin.core.graph.GraphDisplayBrokerPlugin;
import ghidra.app.services.GraphDisplayBroker;
import ghidra.framework.plugintool.PluginTool;
import ghidra.graph.visualization.DefaultGraphDisplayComponentProvider;
import ghidra.service.graph.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.task.TaskMonitor;
public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
private List<String> listenerCalls = new ArrayList<>();
private TestEnv env;
private PluginTool tool;
private AttributedGraph graph;
private ComponentProvider graphComponentProvider;
private GraphDisplay display;
@Before
public void setUp() throws Exception {
env = new TestEnv();
tool = env.launchDefaultTool();
tool.addPlugin(GraphDisplayBrokerPlugin.class.getName());
graph = createGraph();
showGraph();
graphComponentProvider = tool.getComponentProvider("graph");
}
@After
public void tearDown() {
env.dispose();
}
@Test
public void testSelectVertexAction() {
assertTrue(display.getSelectedVertexIds().isEmpty());
DockingActionIf action = getAction(tool, "Select Vertex");
VertexGraphActionContext context =
new VertexGraphActionContext(graphComponentProvider, graph, null, null,
graph.getVertex("B"));
performAction(action, context, true);
Set<String> selectedVertexIds = display.getSelectedVertexIds();
assertEquals(1, selectedVertexIds.size());
assertTrue(selectedVertexIds.contains(graph.getVertex("B").getId()));
// now try and select a second vertex
context = new VertexGraphActionContext(graphComponentProvider, graph, null, null,
graph.getVertex("D"));
performAction(action, context, true);
selectedVertexIds = display.getSelectedVertexIds();
assertEquals(2, selectedVertexIds.size());
assertTrue(selectedVertexIds.contains(graph.getVertex("B").getId()));
assertTrue(selectedVertexIds.contains(graph.getVertex("D").getId()));
}
@Test
public void testDeSelectVertexAction() {
display.selectVertices(Arrays.asList("A", "B", "C", "D"), EventTrigger.API_CALL);
assertEquals(4, display.getSelectedVertexIds().size());
DockingActionIf action = getAction(tool, "Deselect Vertex");
VertexGraphActionContext context =
new VertexGraphActionContext(graphComponentProvider, graph, null, null,
graph.getVertex("B"));
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(3, selectedVerticeIds.size());
assertTrue(selectedVerticeIds.contains(graph.getVertex("A").getId()));
assertTrue(selectedVerticeIds.contains(graph.getVertex("D").getId()));
assertTrue(selectedVerticeIds.contains(graph.getVertex("D").getId()));
assertFalse(selectedVerticeIds.contains(graph.getVertex("B").getId()));
}
@Test
public void testSelectEdgeAction() {
assertTrue(display.getSelectedVertexIds().isEmpty());
DockingActionIf action = getAction(tool, "Select Edge");
EdgeGraphActionContext context =
new EdgeGraphActionContext(graphComponentProvider, graph, null, null,
graph.getEdge(graph.getVertex("A"), graph.getVertex("B")));
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(2, selectedVerticeIds.size());
assertTrue(selectedVerticeIds.contains(graph.getVertex("A").getId()));
assertTrue(selectedVerticeIds.contains(graph.getVertex("B").getId()));
}
@Test
public void testDeSelectEdgeAction() {
DockingActionIf action = getAction(tool, "Select Edge");
EdgeGraphActionContext context =
new EdgeGraphActionContext(graphComponentProvider, graph, null, null,
graph.getEdge(graph.getVertex("A"), graph.getVertex("B")));
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(2, selectedVerticeIds.size());
action = getAction(tool, "Deselect Edge");
performAction(action, context, true);
selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(0, selectedVerticeIds.size());
}
@Test
public void testSelectEdgeSource() {
display.setLocationFocus("D", EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Edge Source");
EdgeGraphActionContext context =
new EdgeGraphActionContext(graphComponentProvider, graph, null, null,
graph.getEdge(graph.getVertex("A"), graph.getVertex("B")));
performAction(action, context, true);
assertEquals("A", display.getFocusedVertexId());
}
@Test
public void testSelectEdgeTarget() {
display.setLocationFocus("D", EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Edge Target");
EdgeGraphActionContext context =
new EdgeGraphActionContext(graphComponentProvider, graph, null, null,
graph.getEdge(graph.getVertex("A"), graph.getVertex("B")));
performAction(action, context, true);
assertEquals("B", display.getFocusedVertexId());
}
@Test
public void testInvertSelection() {
display.selectVertices(List.of("A", "C", "E"), EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Invert Selection");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(3, selectedVerticeIds.size());
assertTrue(selectedVerticeIds.contains("B"));
assertTrue(selectedVerticeIds.contains("D"));
assertTrue(selectedVerticeIds.contains("F"));
}
@Test
public void testGrowSelectionOut() {
display.selectVertices(List.of("A"), EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Grow Selection To Targets");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(3, selectedVerticeIds.size());
assertTrue(selectedVerticeIds.contains("A"));
assertTrue(selectedVerticeIds.contains("B"));
assertTrue(selectedVerticeIds.contains("C"));
}
@Test
public void testGrowSelectionIn() {
display.selectVertices(List.of("D"), EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Grow Selection From Sources");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
Set<String> selectedVerticeIds = display.getSelectedVertexIds();
assertEquals(3, selectedVerticeIds.size());
assertTrue(selectedVerticeIds.contains("D"));
assertTrue(selectedVerticeIds.contains("B"));
assertTrue(selectedVerticeIds.contains("C"));
}
@Test
public void testCreateSubGraph() {
List<DefaultGraphDisplayComponentProvider> graphProviders = tool.getWindowManager()
.getComponentProviders(DefaultGraphDisplayComponentProvider.class);
assertEquals(1, graphProviders.size());
DefaultGraphDisplayComponentProvider original = graphProviders.get(0);
display.selectVertices(List.of("B", "C", "D"), EventTrigger.INTERNAL_ONLY);
DockingActionIf action = getAction(tool, "Create Subgraph");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
graphProviders = tool.getWindowManager()
.getComponentProviders(DefaultGraphDisplayComponentProvider.class);
assertEquals(2, graphProviders.size());
DefaultGraphDisplayComponentProvider newProvider = graphProviders.get(0);
if (newProvider == original) {
newProvider = graphProviders.get(1);
}
GraphActionContext actionContext = (GraphActionContext) newProvider.getActionContext(null);
AttributedGraph newGraph = actionContext.getGraph();
assertEquals(3, newGraph.getVertexCount());
assertFalse(contains(newGraph, "A"));
assertTrue(contains(newGraph, "B"));
assertTrue(contains(newGraph, "C"));
assertTrue(contains(newGraph, "D"));
assertFalse(contains(newGraph, "E"));
assertFalse(contains(newGraph, "F"));
}
private boolean contains(AttributedGraph graph, String vertexId) {
return graph.getVertex(vertexId) != null;
}
private void showGraph() throws Exception {
GraphDisplayBroker broker = tool.getService(GraphDisplayBroker.class);
GraphDisplayProvider service = broker.getGraphDisplayProvider("Default Graph Display");
display = service.getGraphDisplay(false, TaskMonitor.DUMMY);
display.setGraph(graph, "test graph", false, TaskMonitor.DUMMY);
display.setGraphDisplayListener(new TestGraphDisplayListener("test"));
}
class TestGraphDisplayListener implements GraphDisplayListener {
private String name;
TestGraphDisplayListener(String name) {
this.name = name;
}
@Override
public void graphClosed() {
listenerCalls.add(name + ": graph closed");
}
@Override
public void selectionChanged(List<String> vertexIds) {
StringBuilder buf = new StringBuilder();
buf.append(name);
buf.append(": selected: ");
for (String id : vertexIds) {
buf.append(id);
buf.append(",");
}
listenerCalls.add(buf.toString());
}
@Override
public void locationFocusChanged(String vertexId) {
listenerCalls.add(name + ": focus: " + vertexId);
}
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new TestGraphDisplayListener("clone");
}
}
private AttributedGraph createGraph() {
AttributedGraph g = new AttributedGraph();
AttributedVertex a = g.addVertex("A");
AttributedVertex b = g.addVertex("B");
AttributedVertex c = g.addVertex("C");
AttributedVertex d = g.addVertex("D");
AttributedVertex e = g.addVertex("E");
AttributedVertex f = g.addVertex("F");
g.addEdge(a, b);
g.addEdge(a, c);
g.addEdge(b, d);
g.addEdge(b, f);
g.addEdge(c, d);
g.addEdge(d, e);
g.addEdge(e, f);
return g;
}
}