added javadoc and updated jungrapht versions to 1.0-RC9

fixed vertex naming to create labels if no label at address.
removed local vertex renaming
vertex-only selection enabled and grow selection
made yellow edges darker
fix initial layout scaling for subgraph display
address warnings and increase stroke size for vertices
comments and adjusting initial layout area size calculation
multiselection strategy
moved buttons out of function so they can be re-enabled in popup menu
cleaned up and documented selection menus
improve selection following by sending only one event, filtering hidden elements, and disabliing the butten when selection is complete
improve grow selection to grow from all selected vertices
better generics, simplified root predicate. added flag so multiaction does not have to fire an action on the first item
support renaming from the graph view
added rename popup. corrected some errors
use function to control mincross work based on graph size
fixed LayoutWorkingDialog close issue a better way.
use toggle button for magnifier and connect with Lens kill switch
html escape the attributes in case they include things like < and >
changed thread stop and added initial dimension function
This commit is contained in:
Tom Nelson 2020-08-25 15:21:57 +00:00
parent 2892aeadc7
commit 2f447ae0d0
35 changed files with 1317 additions and 355 deletions

13
.gitignore vendored
View File

@ -69,3 +69,16 @@ Release
*/*/*/*/src-gen */*/*/*/src-gen
*/*/*/*/model/generated */*/*/*/model/generated
*/*/*/*/test-bin */*/*/*/test-bin
# Ignore Intellij metadata
**/*.iml
**/.idea
*.iml
.idea
# Ignore gradle wrapper files
gradle/wrapper
gradlew
gradlew.*

View File

@ -19,7 +19,10 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.app.events.*; import ghidra.app.events.*;
import ghidra.framework.cmd.Command;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginEvent; import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
@ -161,6 +164,21 @@ public abstract class AddressBasedGraphDisplayListener
return p == program; 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 @Override
public void domainObjectChanged(DomainObjectChangedEvent ev) { public void domainObjectChanged(DomainObjectChangedEvent ev) {
if (!(ev.containsEvent(ChangeManager.DOCR_SYMBOL_ADDED) || if (!(ev.containsEvent(ChangeManager.DOCR_SYMBOL_ADDED) ||

View File

@ -1,7 +1,9 @@
EXCLUDE FROM GHIDRA JAR: true EXCLUDE FROM GHIDRA JAR: true
MODULE FILE LICENSE: lib/jungrapht-visualization-1.0-RC8.jar BSD MODULE FILE LICENSE: lib/jungrapht-visualization-1.0-SNAPSHOT.jar BSD
MODULE FILE LICENSE: lib/jgrapht-core-1.4.0.jar LGPL 2.1 MODULE FILE LICENSE: lib/jgrapht-core-1.5.0.jar LGPL 2.1
MODULE FILE LICENSE: lib/jgrapht-io-1.4.0.jar LGPL 2.1 MODULE FILE LICENSE: lib/jgrapht-io-1.5.0.jar LGPL 2.1
MODULE FILE LICENSE: lib/jheaps-0.11.jar Apache License 2.0 MODULE FILE LICENSE: lib/jheaps-0.11.jar Apache License 2.0
MODULE FILE LICENSE: lib/log4j-slf4j-impl-2.12.1.jar Apache License 2.0
MODULE FILE LICENSE: lib/slf4j-api-1.7.25.jar MIT MODULE FILE LICENSE: lib/slf4j-api-1.7.25.jar MIT
MODULE FILE LICENSE: lib/slf4j-nop-1.7.25.jar MIT

View File

@ -11,13 +11,17 @@ eclipse.project.name = 'Features Graph Services'
dependencies { dependencies {
compile project(":Base") compile project(":Base")
compile "com.github.tomnelson:jungrapht-visualization:1.0-RC8" compile "com.github.tomnelson:jungrapht-visualization:1.0-RC9"
compile "org.jgrapht:jgrapht-core:1.4.0" compile "com.github.tomnelson:jungrapht-layout:1.0-RC9"
compile "org.jgrapht:jgrapht-core:1.5.0"
// not using jgrapht-io code that depends on antlr, so exclude antlr // not using jgrapht-io code that depends on antlr, so exclude antlr
compile ("org.jgrapht:jgrapht-io:1.4.0") { exclude group: "org.antlr", module: "antlr4-runtime" } compile ("org.jgrapht:jgrapht-io:1.5.0") { exclude group: "org.antlr", module: "antlr4-runtime" }
runtime "org.slf4j:slf4j-api:1.7.25" runtime "org.slf4j:slf4j-api:1.7.25"
// use this if you want no slf4j log messages
runtime "org.slf4j:slf4j-nop:1.7.25" runtime "org.slf4j:slf4j-nop:1.7.25"
// use this if you want slf4j log messages sent to log4j
// runtime "org.apache.logging.log4j:log4j-slf4j-impl:2.12.1"
runtime "org.jheaps:jheaps:0.11" runtime "org.jheaps:jheaps:0.11"
helpPath project(path: ":Base", configuration: 'helpPath') helpPath project(path: ":Base", configuration: 'helpPath')

View File

@ -15,6 +15,7 @@ src/main/help/help/topics/GraphServices/GraphDisplay.htm||GHIDRA||||END|
src/main/help/help/topics/GraphServices/GraphExport.htm||GHIDRA||||END| src/main/help/help/topics/GraphServices/GraphExport.htm||GHIDRA||||END|
src/main/help/help/topics/GraphServices/images/DefaultGraphDisplay.png||GHIDRA||||END| src/main/help/help/topics/GraphServices/images/DefaultGraphDisplay.png||GHIDRA||||END|
src/main/help/help/topics/GraphServices/images/ExportDialog.png||GHIDRA||||END| src/main/help/help/topics/GraphServices/images/ExportDialog.png||GHIDRA||||END|
src/main/resources/images/Lasso.png||GHIDRA||||END|
src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/redspheregraph.png||GHIDRA||||END| src/main/resources/images/redspheregraph.png||GHIDRA||||END|
src/main/resources/images/sat2.png||GHIDRA||||END| src/main/resources/images/sat2.png||GHIDRA||||END|

View File

@ -27,6 +27,8 @@
<ul> <ul>
<li>MouseButton1+drag will translate the display in the x and y axis</li> <li>MouseButton1+drag will translate the display in the x and y axis</li>
<li>Mouse Wheel will zoom in and out</li> <li>Mouse Wheel will zoom in and out</li>
<li>CTRL+Mouse Wheel will zoom in and out in the X-Axis only</li>
<li>ALT+Mouse Wheel will zoom in and out in the Y-Axis only</li>
<li>Ctrl+MouseButton1 will select a vertex or edge</li> <li>Ctrl+MouseButton1 will select a vertex or edge</li>
<ul> <ul>
<li>Shift+Ctrl+MouseButton1 over an unselected vertex will add that vertex to the selection</li> <li>Shift+Ctrl+MouseButton1 over an unselected vertex will add that vertex to the selection</li>
@ -37,30 +39,41 @@
</ul> </ul>
<H2>Upper-right Icon Buttons:</H2> <H2>Upper-right Icon Buttons:</H2>
<ul> <ul>
<li>The <IMG src="images/fingerPointer.png"> 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/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/sat2.png" width="16" height="16"> button 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/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/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"> button will open a rectangular magnification lens in the graph view</li> <li>The <IMG src="images/magnifier.png"> toggle button, when 'set' will open a rectangular magnification lens in the graph view</li>
<ul> <ul>
<li>MouseButton1 click-drag on the lens center circle to move the magnifier lens</li> <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-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>MouseButton1 click on the upper-right circle-cross to dispose of the magnifier lens</li>
<li>Ctrl-MouseWheel to change the magnification of the lens</li> <li>MouseWheel will change the magnification of the lens</li>
</ul> </ul>
<li>The <IMG src="images/view-filter.png"> button will open a Filter dialog. Select buttons in the dialog to hide specific vertices or edges in the display</li> <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>
<ul> <ul>
<li>The Filter dialog buttons are created by examining the graph vertex/edge properties to discover candidates for filtering</li> <li>The Filter dialog buttons are created by examining the graph vertex/edge properties to discover candidates for filtering</li>
</ul> </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>Pull-Down the <IMG src="images/katomic.png" width="16" height="16"> Arrangement menu to select one of several graph layout algorithms.</li>
<ul> <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 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>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>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>Compact Hierarchical is the <b>TidierTree Layout Algorithm</b>. It builds a tree structure and attempts to reduce horizontal space.</li>
<li>Hierarchical MinCross is the <b>Sugiyama Layout Algorithm</b>. It attempts to route edges around vertices in order to reduce crossing.</li>
<li>Hierarchical is a basic Tree algorithm. It prioritizes 'important' edges while constructing the tree.</li>
<li>Hierarchical Multi Row is the Tree algorithm above, but it will create new rows (typewriter fashion) to reduce horizontal spread.</li>
<li>Radial is a Tree structure with the root(s) at the center and child vertices radiating outwards.</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>
</ul> </ul>
</BODY> </BODY>

View File

@ -33,7 +33,7 @@ public class AttributedGraphExporterFactory
AttributedGraphExporterFactory() { AttributedGraphExporterFactory() {
vertexLabelProvider = AttributedVertex::getName; vertexLabelProvider = AttributedVertex::getName;
edgeLabelProvider = Object::toString; edgeLabelProvider = Object::toString;
edgeIdProvider = e -> e.getId(); edgeIdProvider = AttributedEdge::getId;
edgeAttributeProvider = AttributedGraphExporterFactory::getComponentAttributes; edgeAttributeProvider = AttributedGraphExporterFactory::getComponentAttributes;
vertexAttributeProvider = AttributedGraphExporterFactory::getComponentAttributes; vertexAttributeProvider = AttributedGraphExporterFactory::getComponentAttributes;
vertexIdProvider = AttributedVertex::getId; vertexIdProvider = AttributedVertex::getId;
@ -54,7 +54,7 @@ public class AttributedGraphExporterFactory
.entrySet() .entrySet()
.stream() .stream()
.map(entry -> new AbstractMap.SimpleEntry<String, Attribute>(entry.getKey(), .map(entry -> new AbstractMap.SimpleEntry<String, Attribute>(entry.getKey(),
new DefaultAttribute<String>(entry.getValue(), AttributeType.STRING))) new DefaultAttribute<>(entry.getValue(), AttributeType.STRING)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
} }

View File

@ -34,7 +34,7 @@ import ghidra.util.task.TaskMonitor;
*/ */
class ExportAttributedGraphDisplay implements GraphDisplay { class ExportAttributedGraphDisplay implements GraphDisplay {
private PluginTool pluginTool; private final PluginTool pluginTool;
private String description; private String description;
/** /**

View File

@ -52,8 +52,7 @@ public class ExportAttributedGraphDisplayProvider implements GraphDisplayProvide
public GraphDisplay getGraphDisplay(boolean reuseGraph, public GraphDisplay getGraphDisplay(boolean reuseGraph,
TaskMonitor monitor) { TaskMonitor monitor) {
ExportAttributedGraphDisplay display = new ExportAttributedGraphDisplay(this); return new ExportAttributedGraphDisplay(this);
return display;
} }
@Override @Override

View File

@ -20,7 +20,7 @@ enum GraphExportFormat {
DIMACS("col"), DIMACS("col"),
DOT("gv"), DOT("gv"),
GML("gml"), GML("gml"),
GRAPHML("graphhml"), GRAPHML("graphml"),
JSON("json"), JSON("json"),
LEMON("lgf"), LEMON("lgf"),
MATRIX("g"), MATRIX("g"),

View File

@ -59,7 +59,7 @@ public class GraphExporterDialog extends DialogComponentProvider {
private JTextField filePathTextField; private JTextField filePathTextField;
private JButton fileChooserButton; private JButton fileChooserButton;
private GhidraComboBox<GraphExportFormat> comboBox; private GhidraComboBox<GraphExportFormat> comboBox;
private Graph<AttributedVertex, AttributedEdge> graph; private final Graph<AttributedVertex, AttributedEdge> graph;
/** /**
* Construct a new ExporterDialog for exporting a program, optionally only exported a * Construct a new ExporterDialog for exporting a program, optionally only exported a

View File

@ -26,14 +26,14 @@ import ghidra.graph.job.AbstractAnimatorJob;
import ghidra.service.graph.AttributedEdge; import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex; import ghidra.service.graph.AttributedVertex;
public class CenterAnimation extends AbstractAnimatorJob { public class CenterAnimation<V, E> extends AbstractAnimatorJob {
protected int duration = 1000; protected int duration = 1000;
private Point2D oldPoint; private final Point2D oldPoint;
private Point2D newPoint; private final Point2D newPoint;
private Point2D lastPoint = new Point2D.Double(); private final Point2D lastPoint = new Point2D.Double();
private VisualizationViewer<AttributedVertex, AttributedEdge> viewer; private final VisualizationViewer<V, E> viewer;
public CenterAnimation(VisualizationViewer<AttributedVertex, AttributedEdge> viewer, public CenterAnimation(VisualizationViewer<V, E> viewer,
Point2D oldPoint, Point2D newPoint) { Point2D oldPoint, Point2D newPoint) {
this.viewer = viewer; this.viewer = viewer;
this.oldPoint = oldPoint; this.oldPoint = oldPoint;

View File

@ -195,6 +195,11 @@ public abstract class Colors {
*/ */
private static Color blue = new Color(100, 100, 255); private static Color blue = new Color(100, 100, 255);
/**
* a yellow that is darker than {@code Color.yellow}
*/
private static Color darkerYellow = new Color(225, 225, 0);
/** /**
* these are vertex or edge types that have defined colors * these are vertex or edge types that have defined colors
* (the keys are the property values for the vertex/edge keys: * (the keys are the property values for the vertex/edge keys:
@ -229,7 +234,7 @@ public abstract class Colors {
entry("Computed",Color.cyan), entry("Computed",Color.cyan),
entry("Indirection",Color.pink), entry("Indirection",Color.pink),
entry("Unconditional-Jump", Color.green), entry("Unconditional-Jump", Color.green),
entry("Conditional-Jump", Color.yellow), entry("Conditional-Jump", darkerYellow),
entry("Terminator", WEB_COLOR_MAP.get("Purple")), entry("Terminator", WEB_COLOR_MAP.get("Purple")),
entry("Conditional-Return", WEB_COLOR_MAP.get("Purple")) entry("Conditional-Return", WEB_COLOR_MAP.get("Purple"))
); );

View File

@ -29,7 +29,8 @@ final class DefaultDisplayGraphIcons {
public static final Icon SATELLITE_VIEW_ICON = Icons.get("images/sat2.png"); public static final Icon SATELLITE_VIEW_ICON = Icons.get("images/sat2.png");
public static final Icon VIEW_MAGNIFIER_ICON = Icons.get("images/magnifier.png"); public static final Icon VIEW_MAGNIFIER_ICON = Icons.get("images/magnifier.png");
public static final Icon GRAPH_FILTERS_ICON = Icons.get("images/view-filter.png");
public static final Icon PROGRAM_GRAPH_ICON = Icons.get("images/redspheregraph.png"); public static final Icon PROGRAM_GRAPH_ICON = Icons.get("images/redspheregraph.png");
public static final Icon LAYOUT_ALGORITHM_ICON = Icons.get("images/katomic.png"); public static final Icon LAYOUT_ALGORITHM_ICON = Icons.get("images/katomic.png");
public static final Icon LASSO_ICON = Icons.get("images/Lasso.png");
public static final Icon FILTER_ICON = Icons.get("images/filter_on.png");
} }

View File

@ -15,54 +15,97 @@
*/ */
package ghidra.graph.visualization; package ghidra.graph.visualization;
import static org.jungrapht.visualization.renderers.BiModalRenderer.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.swing.*;
import org.jgrapht.Graph;
import org.jungrapht.visualization.*;
import org.jungrapht.visualization.annotations.MultiSelectedVertexPaintable;
import org.jungrapht.visualization.annotations.SingleSelectedVertexPaintable;
import org.jungrapht.visualization.control.*;
import org.jungrapht.visualization.decorators.*;
import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.model.Point;
import org.jungrapht.visualization.renderers.*;
import org.jungrapht.visualization.renderers.Renderer;
import org.jungrapht.visualization.selection.MutableSelectedState;
import org.jungrapht.visualization.transform.*;
import org.jungrapht.visualization.transform.shape.MagnifyImageLensSupport;
import org.jungrapht.visualization.transform.shape.MagnifyShapeTransformer;
import docking.ActionContext; import docking.ActionContext;
import docking.action.builder.*; import docking.action.ToggleDockingAction;
import docking.action.builder.ActionBuilder;
import docking.action.builder.MultiStateActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import docking.menu.ActionState; import docking.menu.ActionState;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.graph.AttributeFilters; import ghidra.graph.AttributeFilters;
import ghidra.graph.job.GraphJobRunner; import ghidra.graph.job.GraphJobRunner;
import ghidra.service.graph.*; import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedGraph;
import ghidra.service.graph.AttributedVertex;
import ghidra.service.graph.DummyGraphDisplayListener;
import ghidra.service.graph.GraphDisplay;
import ghidra.service.graph.GraphDisplayListener;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import org.jgrapht.Graph;
import org.jungrapht.visualization.RenderContext;
import org.jungrapht.visualization.SatelliteVisualizationViewer;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.annotations.MultiSelectedVertexPaintable;
import org.jungrapht.visualization.annotations.SingleSelectedVertexPaintable;
import org.jungrapht.visualization.control.DefaultGraphMouse;
import org.jungrapht.visualization.control.DefaultLensGraphMouse;
import org.jungrapht.visualization.control.DefaultSatelliteGraphMouse;
import org.jungrapht.visualization.control.LensGraphMouse;
import org.jungrapht.visualization.control.LensMagnificationGraphMousePlugin;
import org.jungrapht.visualization.control.MultiSelectionStrategy;
import org.jungrapht.visualization.decorators.EdgeShape;
import org.jungrapht.visualization.decorators.EllipseShapeFunction;
import org.jungrapht.visualization.decorators.IconShapeFunction;
import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.util.InitialDimensionFunction;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.model.Point;
import org.jungrapht.visualization.renderers.JLabelVertexLabelRenderer;
import org.jungrapht.visualization.renderers.LightweightVertexRenderer;
import org.jungrapht.visualization.renderers.ModalRenderer;
import org.jungrapht.visualization.renderers.Renderer;
import org.jungrapht.visualization.selection.MutableSelectedState;
import org.jungrapht.visualization.selection.VertexEndpointsSelectedEdgeSelectedState;
import org.jungrapht.visualization.transform.Lens;
import org.jungrapht.visualization.transform.LensSupport;
import org.jungrapht.visualization.transform.MutableTransformer;
import org.jungrapht.visualization.transform.shape.MagnifyImageLensSupport;
import org.jungrapht.visualization.transform.shape.MagnifyShapeTransformer;
import org.jungrapht.visualization.util.RectangleUtils;
import resources.Icons; import resources.Icons;
import util.CollectionUtils;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JRadioButton;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static org.jungrapht.visualization.MultiLayerTransformer.Layer.*;
import static org.jungrapht.visualization.renderers.BiModalRenderer.LIGHTWEIGHT;
/** /**
* Delegates to a {@link VisualizationViewer} to draw a graph visualization * Delegates to a {@link VisualizationViewer} to draw a graph visualization
*/ */
public class DefaultGraphDisplay implements GraphDisplay { public class DefaultGraphDisplay implements GraphDisplay {
public static final String FAVORED_EDGE = "Fall-Through"; public static final String FAVORED_EDGE = "Fall-Through";
private static final int MAX_NODES = 10000; private static final int MAX_NODES = Integer.getInteger("maxNodes", 10000);
public static final Dimension PREFERRED_VIEW_SIZE = new Dimension(1000, 1000); public static final Dimension PREFERRED_VIEW_SIZE = new Dimension(1000, 1000);
public static final Dimension PREFERRED_LAYOUT_SIZE = new Dimension(3000, 3000); public static final Dimension PREFERRED_LAYOUT_SIZE = new Dimension(3000, 3000);
@ -84,22 +127,22 @@ public class DefaultGraphDisplay implements GraphDisplay {
/** /**
* the delegate viewer to display the ProgramGraph * the delegate viewer to display the ProgramGraph
*/ */
private VisualizationViewer<AttributedVertex, AttributedEdge> viewer; private final VisualizationViewer<AttributedVertex, AttributedEdge> viewer;
/** /**
* the {@link PluginTool} * the {@link PluginTool}
*/ */
private PluginTool pluginTool; private final PluginTool pluginTool;
/** /**
* the {@link Plugin} that manages this {@link GraphDisplay} * the {@link Plugin} that manages this {@link GraphDisplay}
*/ */
private String pluginName = "ProgramGraphPlugin"; private final String pluginName = "ProgramGraphPlugin";
/** /**
* provides the component for the {@link GraphDisplay} * provides the component for the {@link GraphDisplay}
*/ */
private DefaultGraphDisplayComponentProvider componentProvider; private final DefaultGraphDisplayComponentProvider componentProvider;
/** /**
* whether to scroll the visualization in order to center the selected vertex * whether to scroll the visualization in order to center the selected vertex
@ -110,14 +153,65 @@ public class DefaultGraphDisplay implements GraphDisplay {
/** /**
* allows selection of various {@link LayoutAlgorithm} ('arrangements') * allows selection of various {@link LayoutAlgorithm} ('arrangements')
*/ */
private LayoutTransitionManager layoutTransitionManager; private final LayoutTransitionManager<AttributedVertex, AttributedEdge> layoutTransitionManager;
/** /**
* manages highlight painting of a single selected vertex * provides graph displays for supplied graphs
*/ */
private SingleSelectedVertexPaintable<AttributedVertex, AttributedEdge> singleSelectedVertexPaintable; private final DefaultGraphDisplayProvider graphDisplayProvider;
private MultiSelectedVertexPaintable<AttributedVertex, AttributedEdge> multiSelectedVertexPaintable; /**
private DefaultGraphDisplayProvider graphDisplayProvider; * a 'busy' dialog to show while the layout algorithm is working
*/
private LayoutWorkingDialog layoutWorkingDialog;
/**
* the vertex that has been nominated to be 'located' in the graph display and listing
*/
private AttributedVertex locatedVertex;
private final GraphJobRunner jobRunner = new GraphJobRunner();
/**
* a satellite view that shows in the lower left corner as a birds-eye view of the graph display
*/
private final SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> satelliteViewer;
/**
* generated filters on edges
*/
private AttributeFilters edgeFilters;
/**
* generated filters on vertices
*/
private AttributeFilters vertexFilters;
/**
* a dialog populated with generated vertex/edge filters
*/
private FilterDialog filterDialog;
/**
* holds the vertex icons (instead of recomputing them)
*/
private GhidraIconCache iconCache;
/**
* multi-selection is done in a free-form traced shape instead of a rectangle
*/
private boolean freeFormSelection;
/**
* 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 -> {
try {
AttributedGraph attributedGraph = new AttributedGraph();
g.vertexSet().forEach(attributedGraph::addVertex);
g.edgeSet().forEach(e -> {
AttributedVertex source = g.getEdgeSource(e);
AttributedVertex target = g.getEdgeTarget(e);
attributedGraph.addEdge(source, target, e);
});
displaySubGraph(attributedGraph);
} catch (CancelledException e) {
// noop
}
};
/** /**
* Create the initial display, the graph-less visualization viewer, and its controls * Create the initial display, the graph-less visualization viewer, and its controls
@ -129,14 +223,13 @@ public class DefaultGraphDisplay implements GraphDisplay {
this.displayId = id; this.displayId = id;
this.pluginTool = graphDisplayProvider.getPluginTool(); this.pluginTool = graphDisplayProvider.getPluginTool();
this.viewer = createViewer(); this.viewer = createViewer();
buildHighlighers(); buildHighlighers();
componentProvider = new DefaultGraphDisplayComponentProvider(this, pluginTool); componentProvider = new DefaultGraphDisplayComponentProvider(this, pluginTool);
componentProvider.addToTool(); componentProvider.addToTool();
satelliteViewer = createSatelliteViewer(viewer); satelliteViewer = createSatelliteViewer(viewer);
layoutTransitionManager = layoutTransitionManager =
new LayoutTransitionManager(viewer, this::isRoot, this::isFavoredEdge); new LayoutTransitionManager<>(viewer, this::isRoot);
viewer.getComponent().addComponentListener(new ComponentAdapter() { viewer.getComponent().addComponentListener(new ComponentAdapter() {
@Override @Override
@ -150,6 +243,9 @@ public class DefaultGraphDisplay implements GraphDisplay {
} }
}); });
viewer.setInitialDimensionFunction(InitialDimensionFunction
.builder(viewer.getRenderContext().getVertexBoundsFunction()).build());
createActions(); createActions();
connectSelectionStateListeners(); connectSelectionStateListeners();
} }
@ -164,7 +260,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
return displayId; return displayId;
} }
private LensSupport<LensGraphMouse> createManifiers() { /**
* create a magnification lens for the graph display
* @return a {@link LensSupport} for the new magnifier
*/
private LensSupport<LensGraphMouse> createMagnifier() {
Lens lens = Lens.builder().lensShape(Lens.Shape.RECTANGLE).magnification(3.f).build(); Lens lens = Lens.builder().lensShape(Lens.Shape.RECTANGLE).magnification(3.f).build();
lens.setMagnification(2.f); lens.setMagnification(2.f);
LensMagnificationGraphMousePlugin magnificationPlugin = LensMagnificationGraphMousePlugin magnificationPlugin =
@ -172,52 +272,71 @@ public class DefaultGraphDisplay implements GraphDisplay {
MutableTransformer transformer = viewer.getRenderContext() MutableTransformer transformer = viewer.getRenderContext()
.getMultiLayerTransformer() .getMultiLayerTransformer()
.getTransformer(MultiLayerTransformer.Layer.VIEW); .getTransformer(VIEW);
MagnifyShapeTransformer shapeTransformer = MagnifyShapeTransformer.builder(lens) MagnifyShapeTransformer shapeTransformer = MagnifyShapeTransformer.builder(lens)
// this lens' delegate is the viewer's VIEW layer // this lens' delegate is the viewer's VIEW layer
.delegate(transformer) .delegate(transformer)
.build(); .build();
LensGraphMouse lensGraphMouse = new DefaultLensGraphMouse<>(magnificationPlugin);
return MagnifyImageLensSupport.builder(viewer) return MagnifyImageLensSupport.builder(viewer)
.lensTransformer(shapeTransformer) .lensTransformer(shapeTransformer)
.lensGraphMouse(new DefaultLensGraphMouse<>(magnificationPlugin)) .lensGraphMouse(lensGraphMouse)
.build(); .build();
} }
/**
* create the highlighters ({@code Paintable}s to show which vertices have been selected or located
*/
private void buildHighlighers() { private void buildHighlighers() {
// for highlighting of multiple selected vertices // for highlighting of multiple selected vertices
this.multiSelectedVertexPaintable = MultiSelectedVertexPaintable.builder(viewer) MultiSelectedVertexPaintable<AttributedVertex, AttributedEdge> multiSelectedVertexPaintable = MultiSelectedVertexPaintable.builder(viewer)
.selectionStrokeMin(4.f) .selectionStrokeMin(4.f)
.selectionPaint(Color.red) .selectionPaint(Color.red)
.useBounds(true) .useBounds(false)
.build(); .build();
// for highlighting of single 'located' vertices
this.singleSelectedVertexPaintable = SingleSelectedVertexPaintable.builder(viewer) // manages highlight painting of a single selected vertex
SingleSelectedVertexPaintable<AttributedVertex, AttributedEdge> singleSelectedVertexPaintable = SingleSelectedVertexPaintable.builder(viewer)
.selectionStrokeMin(4.f) .selectionStrokeMin(4.f)
.selectionPaint(Color.red) .selectionPaint(Color.red)
.selectedVertexFunction(vs -> this.locatedVertex)
.build(); .build();
// this draws the selection highlights // draws the selection highlights
viewer.addPostRenderPaintable(multiSelectedVertexPaintable); viewer.addPostRenderPaintable(multiSelectedVertexPaintable);
// this draws the location arrow // draws the location arrow
viewer.addPostRenderPaintable(singleSelectedVertexPaintable); viewer.addPostRenderPaintable(singleSelectedVertexPaintable);
} }
/**
* create the action icon buttons on the upper-right of the graph display window
*/
private void createActions() { private void createActions() {
// create a toggle for 'scroll to selected vertex' // create a toggle for 'scroll to selected vertex'
new ToggleActionBuilder("Scroll To Selection", pluginName) new ToggleActionBuilder("Scroll To Selection", pluginName)
.toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON) .toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON)
.description("Scroll to Selection") .description("Scroll display to center the 'Located' vertex")
.selected(false) .selected(false)
.onAction(context -> enableScrollToSelection = .onAction(context -> enableScrollToSelection =
((AbstractButton) context.getSourceObject()).isSelected()) ((AbstractButton) context.getSourceObject()).isSelected())
.buildAndInstallLocal(componentProvider); .buildAndInstallLocal(componentProvider);
// 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)
.toolBarIcon(DefaultDisplayGraphIcons.LASSO_ICON)
.description("Trace Free-Form Shape to select multiple vertices (CTRL-click-drag)")
.selected(false)
.onAction(context ->
freeFormSelection = ((AbstractButton) context.getSourceObject()).isSelected())
.buildAndInstallLocal(componentProvider);
// create an icon button to display the satellite view // create an icon button to display the satellite view
new ToggleActionBuilder("SatelliteView", pluginName).description("Show Satellite View") new ToggleActionBuilder("SatelliteView", pluginName).description("Show Satellite View")
.toolBarIcon(DefaultDisplayGraphIcons.SATELLITE_VIEW_ICON) .toolBarIcon(DefaultDisplayGraphIcons.SATELLITE_VIEW_ICON)
@ -225,56 +344,81 @@ public class DefaultGraphDisplay implements GraphDisplay {
.buildAndInstallLocal(componentProvider); .buildAndInstallLocal(componentProvider);
// create an icon button to reset the view transformations to identity (scaled to layout) // create an icon button to reset the view transformations to identity (scaled to layout)
new ActionBuilder("Reset View", pluginName).description("Reset all view transforms") new ActionBuilder("Reset View", pluginName)
.description("Reset all view transforms to center graph in display")
.toolBarIcon(Icons.REFRESH_ICON) .toolBarIcon(Icons.REFRESH_ICON)
.onAction(context -> { .onAction(context -> viewer.scaleToLayout())
viewer.reset();
viewer.scaleToLayout(true);
})
.buildAndInstallLocal(componentProvider); .buildAndInstallLocal(componentProvider);
// create a button to show the view magnify lens // create a button to show the view magnify lens
LensSupport<LensGraphMouse> magnifyViewSupport = createManifiers(); LensSupport<LensGraphMouse> magnifyViewSupport = createMagnifier();
@SuppressWarnings("unchecked") ToggleDockingAction lensToggle = new ToggleActionBuilder("View Magnifier", pluginName)
LensSupport<LensGraphMouse>[] lenses = new LensSupport[] { magnifyViewSupport }; .description("Show View Magnifier")
new ActionBuilder("View Magnifier", pluginName).description("Show View Magnifier")
.toolBarIcon(DefaultDisplayGraphIcons.VIEW_MAGNIFIER_ICON) .toolBarIcon(DefaultDisplayGraphIcons.VIEW_MAGNIFIER_ICON)
.onAction(context -> { .onAction(context -> magnifyViewSupport.activate(
Arrays.stream(lenses).forEach(LensSupport::deactivate); ((AbstractButton) context.getSourceObject()).isSelected()
magnifyViewSupport.activate(); ))
}) .build();
.buildAndInstallLocal(componentProvider); magnifyViewSupport.addItemListener(itemEvent ->
lensToggle.setSelected(itemEvent.getStateChange() == ItemEvent.SELECTED));
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", pluginName).description("Show Graph Filters")
.toolBarIcon(Icons.CONFIGURE_FILTER_ICON) .toolBarIcon(DefaultDisplayGraphIcons.FILTER_ICON)
.onAction(context -> showFilterDialog()) .onAction(context -> showFilterDialog())
.buildAndInstallLocal(componentProvider); .buildAndInstallLocal(componentProvider);
// create a menu with graph layout algorithm selections
new MultiStateActionBuilder<String>("Arrangement", pluginName) new MultiStateActionBuilder<String>("Arrangement", pluginName)
.description("Select Layout Arrangement") .description("Select Layout Arrangement Algorithm")
.toolBarIcon(DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON) .toolBarIcon(DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON)
.fireFirstAction(false)
.onActionStateChanged((s, t) -> layoutChanged(s.getName())) .onActionStateChanged((s, t) -> layoutChanged(s.getName()))
.addStates(getLayoutActionStates()) .addStates(getLayoutActionStates())
.buildAndInstallLocal(componentProvider); .buildAndInstallLocal(componentProvider);
// show a 'busy' dialog while the layout algorithm is computing vertex locations
viewer.getVisualizationModel().getLayoutModel()
.getLayoutStateChangeSupport().addLayoutStateChangeListener(
evt -> {
if (evt.active) {
Swing.runLater(this::showLayoutWorking);
} else {
Swing.runLater(this::hideLayoutWorking);
}
}
);
} }
/**
* get a {@code List} of {@code ActionState} buttons for the
* configured layout algorithms
* @return a {@code List} of {@code ActionState} buttons
*/
private List<ActionState<String>> getLayoutActionStates() { private List<ActionState<String>> getLayoutActionStates() {
String[] names = layoutTransitionManager.getLayoutNames(); String[] names = layoutTransitionManager.getLayoutNames();
List<ActionState<String>> actionStates = new ArrayList<>(); List<ActionState<String>> actionStates = new ArrayList<>();
for (String layoutName : names) { for (String layoutName : names) {
actionStates.add(new ActionState<String>(layoutName, actionStates.add(new ActionState<>(layoutName,
DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON, layoutName)); DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON, layoutName));
} }
return actionStates; return actionStates;
} }
/**
* respond to a change in the layout name
* @param layoutName the name of the layout algorithm to apply
*/
private void layoutChanged(String layoutName) { private void layoutChanged(String layoutName) {
if (layoutTransitionManager != null) { if (layoutTransitionManager != null) {
layoutTransitionManager.setLayout(layoutName); layoutTransitionManager.setLayout(layoutName);
} }
} }
/**
* show the dialog with generated filters
*/
private void showFilterDialog() { private void showFilterDialog() {
if (filterDialog == null) { if (filterDialog == null) {
if (vertexFilters == null) { if (vertexFilters == null) {
@ -286,6 +430,31 @@ public class DefaultGraphDisplay implements GraphDisplay {
componentProvider.getTool().showDialog(filterDialog); componentProvider.getTool().showDialog(filterDialog);
} }
/**
* show the 'busy' dialog indicating that the layout algorithm is working
*/
protected void showLayoutWorking() {
if (this.layoutWorkingDialog != null) {
layoutWorkingDialog.close();
}
this.layoutWorkingDialog =
new LayoutWorkingDialog(viewer.getVisualizationModel().getLayoutAlgorithm());
componentProvider.getTool().showDialog(layoutWorkingDialog);
}
/**
* hide the 'busy' dialog for the layout algorithm work
*/
protected void hideLayoutWorking() {
if (this.layoutWorkingDialog != null) {
layoutWorkingDialog.close();
}
}
/**
* add or remove the satellite viewer
* @param context information about the event
*/
private void toggleSatellite(ActionContext context) { private void toggleSatellite(ActionContext context) {
if (((AbstractButton) context.getSourceObject()).isSelected()) { if (((AbstractButton) context.getSourceObject()).isSelected()) {
viewer.getComponent().add(satelliteViewer.getComponent()); viewer.getComponent().add(satelliteViewer.getComponent());
@ -294,7 +463,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
viewer.getComponent().remove(satelliteViewer.getComponent()); viewer.getComponent().remove(satelliteViewer.getComponent());
} }
viewer.repaint(); viewer.repaint();
}
/**
* from the supplied {@link Graph}, create a new GraphDisplay in a new window or tab
* @param subGraph
* @throws CancelledException
*/
private void displaySubGraph(Graph<AttributedVertex, AttributedEdge> subGraph) throws CancelledException {
GraphDisplay graphDisplay = graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
graphDisplay.setGraph((AttributedGraph)subGraph, "SubGraph", false, TaskMonitor.DUMMY);
graphDisplay.setGraphDisplayListener(listener);
} }
/** /**
@ -304,11 +483,14 @@ public class DefaultGraphDisplay implements GraphDisplay {
*/ */
private SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> createSatelliteViewer( private SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> createSatelliteViewer(
VisualizationViewer<AttributedVertex, AttributedEdge> parentViewer) { VisualizationViewer<AttributedVertex, AttributedEdge> parentViewer) {
Dimension viewerSize = parentViewer.getSize();
Dimension satelliteSize = new Dimension(
viewerSize.width / 4, viewerSize.height / 4);
final SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> satelliteViewer = final SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> satelliteViewer =
SatelliteVisualizationViewer.builder(parentViewer) SatelliteVisualizationViewer.builder(parentViewer)
.viewSize(new Dimension(250, 250)) .viewSize(satelliteSize)
.build(); .build();
satelliteViewer.setGraphMouse(new DefaultSatelliteGraphMouse<>()); satelliteViewer.setGraphMouse(new DefaultSatelliteGraphMouse());
satelliteViewer.getRenderContext().setEdgeDrawPaintFunction(Colors::getColor); satelliteViewer.getRenderContext().setEdgeDrawPaintFunction(Colors::getColor);
satelliteViewer.getRenderContext() satelliteViewer.getRenderContext()
.setEdgeStrokeFunction(ProgramGraphFunctions::getEdgeStroke); .setEdgeStrokeFunction(ProgramGraphFunctions::getEdgeStroke);
@ -316,9 +498,20 @@ public class DefaultGraphDisplay implements GraphDisplay {
satelliteViewer.scaleToLayout(); satelliteViewer.scaleToLayout();
satelliteViewer.getRenderContext().setVertexLabelFunction(n -> null); satelliteViewer.getRenderContext().setVertexLabelFunction(n -> null);
satelliteViewer.getComponent().setBorder(BorderFactory.createEtchedBorder()); satelliteViewer.getComponent().setBorder(BorderFactory.createEtchedBorder());
parentViewer.getComponent().addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent evt) {
Dimension size = evt.getComponent().getSize();
Dimension quarterSize = new Dimension(size.width / 4, size.height / 4);
satelliteViewer.getComponent().setSize(quarterSize);
}
});
return satelliteViewer; return satelliteViewer;
} }
/**
* close this graph display
*/
@Override @Override
public void close() { public void close() {
graphDisplayProvider.remove(this); graphDisplayProvider.remove(this);
@ -328,12 +521,23 @@ public class DefaultGraphDisplay implements GraphDisplay {
listener = null; listener = null;
} }
/**
* accept a {@code GraphDisplayListener}
* @param listener the listener to be notified
*/
@Override @Override
public void setGraphDisplayListener(GraphDisplayListener listener) { public void setGraphDisplayListener(GraphDisplayListener listener) {
if (this.listener != null) { if (this.listener != null) {
this.listener.graphClosed(); this.listener.graphClosed();
} }
this.listener = listener; this.listener = listener;
DefaultGraphMouse<AttributedVertex, AttributedEdge> graphMouse =
new GhidraGraphMouse<>(viewer,
subgraphConsumer,
listener,
AttributedVertex::getId,
AttributedVertex::getName);
viewer.setGraphMouse(graphMouse);
} }
/** /**
@ -341,14 +545,21 @@ public class DefaultGraphDisplay implements GraphDisplay {
*/ */
private void connectSelectionStateListeners() { private void connectSelectionStateListeners() {
viewer.getSelectedVertexState().addItemListener(e -> Swing.runLater(() -> { viewer.getSelectedVertexState().addItemListener(e -> Swing.runLater(() -> {
// there was a change in the set of selected vertices.
// if the locatedVertex is null, set it from one of the selected
// vertices
if (e.getStateChange() == ItemEvent.SELECTED) { if (e.getStateChange() == ItemEvent.SELECTED) {
Collection<AttributedVertex> selectedVertices = getVertices(e.getItem()); Collection<AttributedVertex> selectedVertices = getVertices(e.getItem());
List<String> selectedVertexIds = toVertexIds(selectedVertices); List<String> selectedVertexIds = toVertexIds(selectedVertices);
notifySelectionChanged(selectedVertexIds); notifySelectionChanged(selectedVertexIds);
AttributedVertex vertex = CollectionUtils.any(selectedVertices); if (selectedVertices.size() == 1) {
if (vertex != null) { // if only one vertex was selected, make it the locatedVertex
notifyLocationChanged(vertex.getId()); setLocatedVertex(selectedVertices.stream().findFirst().get());
} else if (this.locatedVertex == null) {
// if there is currently no locatedVertex, attempt to get
// one from the selectedVertices
setLocatedVertex(selectedVertices.stream().findFirst().orElse(null));
} }
} }
else if (e.getStateChange() == ItemEvent.DESELECTED) { else if (e.getStateChange() == ItemEvent.DESELECTED) {
@ -358,11 +569,27 @@ public class DefaultGraphDisplay implements GraphDisplay {
})); }));
} }
private List<String> toVertexIds(Collection<AttributedVertex> selectedVertices) { /**
return selectedVertices.stream().map(v -> v.getId()).collect(Collectors.toList()); * set the vertex that has been nominated to be 'located'
* @param vertex the lucky vertex
*/
protected void setLocatedVertex(AttributedVertex vertex) {
boolean changed = this.locatedVertex != vertex;
this.locatedVertex = vertex;
if (locatedVertex != null && changed) {
notifyLocationChanged(locatedVertex.getId());
}
}
/**
* transform the supplied {@code AttributedVertex} Set members to a List of their ids
* @param selectedVertices
* @return
*/
private List<String> toVertexIds(Collection<AttributedVertex> selectedVertices) {
return selectedVertices.stream().map(AttributedVertex::getId).collect(Collectors.toList());
} }
@SuppressWarnings("unchecked")
private Collection<AttributedVertex> getVertices(Object item) { private Collection<AttributedVertex> getVertices(Object item) {
if (item instanceof Collection) { if (item instanceof Collection) {
return (Collection<AttributedVertex>) item; return (Collection<AttributedVertex>) item;
@ -373,10 +600,18 @@ public class DefaultGraphDisplay implements GraphDisplay {
return Collections.emptyList(); return Collections.emptyList();
} }
/**
* fire an event to say the selected vertices changed
* @param vertexIds
*/
private void notifySelectionChanged(List<String> vertexIds) { private void notifySelectionChanged(List<String> vertexIds) {
Swing.runLater(() -> listener.selectionChanged(vertexIds)); Swing.runLater(() -> listener.selectionChanged(vertexIds));
} }
/**
* fire and event to say the located vertex changed
* @param vertexId
*/
private void notifyLocationChanged(String vertexId) { private void notifyLocationChanged(String vertexId) {
Swing.runLater(() -> listener.locationChanged(vertexId)); Swing.runLater(() -> listener.locationChanged(vertexId));
} }
@ -400,6 +635,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
viewer.repaint(); viewer.repaint();
} }
/**
*
* @param vertexIds vertex ids of interest
* @return a {@code Set} containing the {@code AttributedVertex} for ths supplied ids
*/
private Set<AttributedVertex> getVertices(Collection<String> vertexIds) { private Set<AttributedVertex> getVertices(Collection<String> vertexIds) {
Set<String> vertexSet = new HashSet<>(vertexIds); Set<String> vertexSet = new HashSet<>(vertexIds);
return graph.vertexSet() return graph.vertexSet()
@ -408,13 +648,21 @@ public class DefaultGraphDisplay implements GraphDisplay {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
/**
* for the supplied vertex id, find the {@code AttributedVertex} and translate
* the display to center it
* @param vertexID the id of the vertex to focus
*/
@Override @Override
public void setLocation(String vertexID) { public void setLocation(String vertexID) {
Optional<AttributedVertex> selected = Optional<AttributedVertex> located =
graph.vertexSet().stream().filter(v -> vertexID.equals(v.getId())).findFirst(); graph.vertexSet().stream().filter(v -> vertexID.equals(v.getId())).findFirst();
log.fine("picking address:" + vertexID + " returned " + selected); log.fine("picking address:" + vertexID + " returned " + located);
viewer.repaint(); viewer.repaint();
selected.ifPresent(this::scrollToSelected); located.ifPresent(v -> {
setLocatedVertex(v);
scrollToSelected(v);
});
viewer.repaint(); viewer.repaint();
} }
@ -425,21 +673,19 @@ public class DefaultGraphDisplay implements GraphDisplay {
private void doSetGraphData(AttributedGraph attributedGraph) { private void doSetGraphData(AttributedGraph attributedGraph) {
graph = attributedGraph; graph = attributedGraph;
layoutTransitionManager.setGraph(graph); layoutTransitionManager.setEdgeComparator(new EdgeComparator(graph, "EdgeType",
DefaultGraphDisplay.FAVORED_EDGE));
configureViewerPreferredSize(); configureViewerPreferredSize();
Swing.runNow(() -> viewer.getVisualizationModel().setGraph(graph)); Swing.runNow(() -> {
// set the graph but defer the layoutalgorithm setting
configureFilters(); viewer.getVisualizationModel().setGraph(graph, false);
configureFilters();
LayoutAlgorithm<AttributedVertex> initialLayoutAlgorithm = LayoutAlgorithm<AttributedVertex> initialLayoutAlgorithm =
layoutTransitionManager.getInitialLayoutAlgorithm(graph); layoutTransitionManager.getInitialLayoutAlgorithm();
viewer.getVisualizationModel().setLayoutAlgorithm(initialLayoutAlgorithm);
viewer.getVisualizationModel().setLayoutAlgorithm(initialLayoutAlgorithm); });
viewer.scaleToLayout();
componentProvider.setVisible(true); componentProvider.setVisible(true);
} }
@ -451,16 +697,12 @@ public class DefaultGraphDisplay implements GraphDisplay {
*/ */
private boolean isRoot(AttributedVertex vertex) { private boolean isRoot(AttributedVertex vertex) {
Set<AttributedEdge> incomingEdgesOf = graph.incomingEdgesOf(vertex); Set<AttributedEdge> incomingEdgesOf = graph.incomingEdgesOf(vertex);
if (incomingEdgesOf.isEmpty()) { return incomingEdgesOf.isEmpty();
return true;
}
Set<AttributedEdge> outgoingEdgesOf = graph.outgoingEdgesOf(vertex);
return outgoingEdgesOf.stream().anyMatch(this::isFavoredEdge) &&
incomingEdgesOf.stream().noneMatch(this::isFavoredEdge);
} }
/**
* configure filters for the graph, based on the vertex and edge attributes
*/
private void configureFilters() { private void configureFilters() {
// close and rebuild filter dialog if exists // close and rebuild filter dialog if exists
if (filterDialog != null) { if (filterDialog != null) {
@ -502,10 +744,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
e -> e.getAttributeMap().values().stream().noneMatch(selected::contains)); e -> e.getAttributeMap().values().stream().noneMatch(selected::contains));
viewer.repaint(); viewer.repaint();
}); });
} }
/**
* configure a preferred size based on the size of the graph to display
*/
private void configureViewerPreferredSize() { private void configureViewerPreferredSize() {
int vertexCount = graph.vertexSet().size(); int vertexCount = graph.vertexSet().size();
// attempt to set a reasonable size for the layout based on the number of vertices // attempt to set a reasonable size for the layout based on the number of vertices
@ -541,6 +784,13 @@ public class DefaultGraphDisplay implements GraphDisplay {
// this would have to set the label function, the label font function // this would have to set the label function, the label font function
} }
/**
* consume a {@link Graph} and display it
* @param graph the graph to display or consume
* @param description a description of the graph
* @param append if true, append the new graph to any existing graph.
* @param monitor a {@link TaskMonitor} which can be used to cancel the graphing operation
*/
@Override @Override
public void setGraph(AttributedGraph graph, String description, boolean append, public void setGraph(AttributedGraph graph, String description, boolean append,
TaskMonitor monitor) { TaskMonitor monitor) {
@ -578,8 +828,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
return newGraph; return newGraph;
} }
/**
* cause the graph to be centered and scaled nicely for the view window
*/
public void centerAndScale() { public void centerAndScale() {
viewer.reset();
viewer.scaleToLayout(); viewer.scaleToLayout();
} }
@ -607,7 +859,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
.getMultiLayerTransformer() .getMultiLayerTransformer()
.inverseTransform(viewer.getCenter()); .inverseTransform(viewer.getCenter());
jobRunner.schedule(new CenterAnimation(viewer, existingCenter, newCenter)); jobRunner.schedule(new CenterAnimation<>(viewer, existingCenter, newCenter));
} }
/**w /**w
@ -620,6 +872,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
scrollToSelected(vertices); scrollToSelected(vertices);
} }
/**
* compute the centroid of a group of vertices, or the center of the graph display
* @param vertices a collection of vertices from which to compute the centroid from their locations
* @return the {@code Point2D} that is the center
*/
private Point2D getPointToCenter(Collection<AttributedVertex> vertices) { private Point2D getPointToCenter(Collection<AttributedVertex> vertices) {
LayoutModel<AttributedVertex> layoutModel = viewer.getVisualizationModel().getLayoutModel(); LayoutModel<AttributedVertex> layoutModel = viewer.getVisualizationModel().getLayoutModel();
@ -638,57 +895,76 @@ public class DefaultGraphDisplay implements GraphDisplay {
return new Point2D.Double(p.x, p.y); return new Point2D.Double(p.x, p.y);
} }
private GraphJobRunner jobRunner = new GraphJobRunner(); /**
private SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> satelliteViewer; * process a request to update the name attribute value of the vertex with the
private AttributeFilters edgeFilters; * supplied id
private AttributeFilters vertexFilters; * @param id the vertix id
private FilterDialog filterDialog; * @param newName the new name of the vertex
private GhidraIconCache iconCache; */
@Override @Override
public void updateVertexName(String id, String newName) { public void updateVertexName(String id, String newName) {
// unsupported // find the vertex, if present, change the name
Optional<AttributedVertex> optional = graph.vertexSet().stream()
.filter(v -> v.getId().equals(id)).findFirst();
if (optional.isPresent()) {
AttributedVertex vertex = optional.get();
vertex.setName(newName);
vertex.clearCache();
iconCache.evict(vertex);
viewer.repaint();
}
} }
/**
*
* @return a description of this graph
*/
@Override @Override
public String getGraphDescription() { public String getGraphDescription() {
return description; return description;
} }
private boolean isFavoredEdge(AttributedEdge edge) { /**
if (edge.getAttributeMap().containsKey("EdgeType")) { * create and return a {@link VisualizationViewer} to display graphs
return edge.getAttributeMap().getOrDefault("EdgeType", "NOTEQUAL").equals(FAVORED_EDGE); * @return
} */
return true;
}
public VisualizationViewer<AttributedVertex, AttributedEdge> createViewer() { public VisualizationViewer<AttributedVertex, AttributedEdge> createViewer() {
final VisualizationViewer<AttributedVertex, AttributedEdge> vv = final VisualizationViewer<AttributedVertex, AttributedEdge> vv =
VisualizationViewer.<AttributedVertex, AttributedEdge> builder() VisualizationViewer.<AttributedVertex, AttributedEdge> builder()
.multiSelectionStrategySupplier(() -> freeFormSelection ?
MultiSelectionStrategy.arbitrary() : MultiSelectionStrategy.rectangular())
.viewSize(PREFERRED_VIEW_SIZE) .viewSize(PREFERRED_VIEW_SIZE)
.layoutSize(PREFERRED_LAYOUT_SIZE) .layoutSize(PREFERRED_LAYOUT_SIZE)
.build(); .build();
// Add a component listener to scale and center the graph after the component // Add an ancestor listener to scale and center the graph after the component
// has been initially sized. Remove the listener after the first time so that any // has been initially shown.
// subsequent resizing does not affect the graph. vv.getComponent().addAncestorListener(new AncestorListener() {
vv.getComponent().addComponentListener(new ComponentAdapter() {
@Override @Override
public void componentResized(ComponentEvent e) { public void ancestorAdded(AncestorEvent ancestorEvent) {
vv.getComponent().removeComponentListener(this); vv.getComponent().removeAncestorListener(this);
Swing.runLater(() -> { Swing.runLater(() -> {
vv.reset();
vv.scaleToLayout(); vv.scaleToLayout();
}); });
} }
@Override
public void ancestorRemoved(AncestorEvent ancestorEvent) {
}
@Override
public void ancestorMoved(AncestorEvent ancestorEvent) {
}
}); });
this.iconCache = new GhidraIconCache();
vv.setVertexToolTipFunction(AttributedVertex::getHtmlString); vv.setVertexToolTipFunction(AttributedVertex::getHtmlString);
vv.setEdgeToolTipFunction(AttributedEdge::getHtmlString); vv.setEdgeToolTipFunction(AttributedEdge::getHtmlString);
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext(); RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
iconCache = new GhidraIconCache();
// set up the shape and color functions // set up the shape and color functions
IconShapeFunction<AttributedVertex> nodeImageShapeFunction = IconShapeFunction<AttributedVertex> nodeImageShapeFunction =
new IconShapeFunction<>(new EllipseShapeFunction<>()); new IconShapeFunction<>(new EllipseShapeFunction<>());
@ -700,6 +976,15 @@ public class DefaultGraphDisplay implements GraphDisplay {
renderContext.setVertexShapeFunction(nodeImageShapeFunction); renderContext.setVertexShapeFunction(nodeImageShapeFunction);
renderContext.setVertexIconFunction(iconCache::get); renderContext.setVertexIconFunction(iconCache::get);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(nodeImageShapeFunction.andThen(s -> RectangleUtils.convert(s.getBounds2D()))).build());
// the selectedEdgeState will be controlled by the vertices that are selected.
// if both endpoints of an edge are selected, select that edge.
vv.setSelectedEdgeState(
new VertexEndpointsSelectedEdgeSelectedState<>(vv.getVisualizationModel()::getGraph,
vv.getSelectedVertexState()));
// selected edges will be drawn with a wider stroke // selected edges will be drawn with a wider stroke
renderContext.setEdgeStrokeFunction( renderContext.setEdgeStrokeFunction(
e -> renderContext.getSelectedEdgeState().isSelected(e) ? new BasicStroke(20.f) e -> renderContext.getSelectedEdgeState().isSelected(e) ? new BasicStroke(20.f)
@ -731,14 +1016,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
renderContext.setEdgeShapeFunction(EdgeShape.line()); renderContext.setEdgeShapeFunction(EdgeShape.line());
DefaultGraphMouse<AttributedVertex, AttributedEdge> graphMouse = new DefaultGraphMouse<>();
vv.setGraphMouse(graphMouse);
vv.getComponent().requestFocus(); vv.getComponent().requestFocus();
vv.setBackground(Color.WHITE); vv.setBackground(Color.WHITE);
return vv; return vv;
} }
/**
* a way to sort attributed vertices or edges based on attribute values
*/
} }

View File

@ -28,7 +28,7 @@ public class DefaultGraphDisplayComponentProvider extends ComponentProviderAdapt
static final String WINDOW_GROUP = "ProgramGraph"; static final String WINDOW_GROUP = "ProgramGraph";
private static final String WINDOW_MENU_GROUP_NAME = "Graph"; private static final String WINDOW_MENU_GROUP_NAME = "Graph";
private DefaultGraphDisplay display; private final DefaultGraphDisplay display;
DefaultGraphDisplayComponentProvider(DefaultGraphDisplay display, PluginTool pluginTool) { DefaultGraphDisplayComponentProvider(DefaultGraphDisplay display, PluginTool pluginTool) {
super(pluginTool, "Graph", "DefaultGraphDisplay"); super(pluginTool, "Graph", "DefaultGraphDisplay");

View File

@ -28,7 +28,7 @@ import ghidra.util.task.TaskMonitor;
public class DefaultGraphDisplayProvider implements GraphDisplayProvider { public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
private Set<DefaultGraphDisplay> displays = new HashSet<>(); private final Set<DefaultGraphDisplay> displays = new HashSet<>();
private PluginTool pluginTool; private PluginTool pluginTool;
private Options options; private Options options;
private int displayCounter; private int displayCounter;

View File

@ -22,7 +22,7 @@ import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedGraph; import ghidra.service.graph.AttributedGraph;
public class EdgeComparator implements Comparator<AttributedEdge> { public class EdgeComparator implements Comparator<AttributedEdge> {
private Set<AttributedEdge> prioritized; private final Set<AttributedEdge> prioritized;
public EdgeComparator(AttributedGraph graph, String attributeName, String value) { public EdgeComparator(AttributedGraph graph, String attributeName, String value) {
prioritized = graph.edgeSet() prioritized = graph.edgeSet()

View File

@ -41,7 +41,7 @@ public class FilterDialog extends DialogComponentProvider {
/** /**
* A {@code List} (possibly empty) of filter buttons for vertices * A {@code List} (possibly empty) of filter buttons for vertices
*/ */
private List<? extends AbstractButton> vertexButtons; private final List<? extends AbstractButton> vertexButtons;
/** /**
* A {@code List} (possibly empty) of filter buttons for edges * A {@code List} (possibly empty) of filter buttons for edges

View File

@ -0,0 +1,158 @@
/* ###
* 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.AbstractPopupGraphMousePlugin;
import org.jungrapht.visualization.control.DefaultGraphMouse;
import org.jungrapht.visualization.control.GraphElementAccessor;
import org.jungrapht.visualization.control.TransformSupport;
import org.jungrapht.visualization.control.VertexSelectingGraphMousePlugin;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.selection.ShapePickSupport;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jungrapht.visualization.VisualizationServer.PREFIX;
/**
* 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> {
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;
/**
* 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 with default values
*/
public GhidraGraphMouse(VisualizationViewer<V, E> viewer,
Consumer<Graph<V, E>> subgraphConsumer,
GraphDisplayListener graphDisplayListener,
Function<V, String> vertexIdFunction,
Function<V, String> vertexNameFunction) {
super(DefaultGraphMouse.<V, E>builder().vertexSelectionOnly(true));
this.viewer = viewer;
this.subgraphConsumer = subgraphConsumer;
this.graphDisplayListener = graphDisplayListener;
this.vertexIdFunction = vertexIdFunction;
this.vertexNameFunction = vertexNameFunction;
}
/**
* create the plugins, and load them
*/
@Override
public void loadPlugins() {
add(new PopupPlugin<>(viewer, subgraphConsumer, graphDisplayListener,
vertexIdFunction, vertexNameFunction));
super.loadPlugins();
}
static class PopupPlugin<V, E> extends AbstractPopupGraphMousePlugin {
VisualizationViewer<V, E> viewer;
Consumer<Graph<V, E>> subgraphConsumer;
GraphDisplayListener graphDisplayListener;
Function<V, String> vertexIdFunction;
Function<V, String> vertexNameFunction;
SelectionFilterMenu<V, E> selectionFilterMenu;
PopupPlugin(VisualizationViewer<V, E> viewer,
Consumer<Graph<V, E>> subgraphConsumer,
GraphDisplayListener graphDisplayListener,
Function<V, String> vertexIdFunction,
Function<V, String> vertexNameFunction
) {
this.viewer = viewer;
this.subgraphConsumer = subgraphConsumer;
this.graphDisplayListener = graphDisplayListener;
this.vertexIdFunction = vertexIdFunction;
this.vertexNameFunction = vertexNameFunction;
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,
pickSize,
pickSize);
LayoutModel<V> layoutModel = viewer.getVisualizationModel().getLayoutModel();
GraphElementAccessor<V, E> pickSupport = viewer.getPickSupport();
V pickedVertex;
if (pickSupport instanceof ShapePickSupport) {
ShapePickSupport<V, E> shapePickSupport =
(ShapePickSupport<V, E>) pickSupport;
pickedVertex = shapePickSupport.getVertex(layoutModel, footprintRectangle);
} else {
TransformSupport<V, E> transformSupport = viewer.getTransformSupport();
Point2D layoutPoint = transformSupport.inverseTransform(viewer, e.getPoint());
pickedVertex = pickSupport.getVertex(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 {
selectionFilterMenu.show(viewer.getComponent(), e.getX(), e.getY());
}
}
}
}

View File

@ -30,19 +30,18 @@ import ghidra.service.graph.AttributedVertex;
public class GhidraIconCache { public class GhidraIconCache {
private static final int DEFAULT_STROKE_THICKNESS = 8; private static final int DEFAULT_STROKE_THICKNESS = 12;
private static final int DEFAULT_FONT_SIZE = 12; private static final int DEFAULT_FONT_SIZE = 12;
private static final String DEFAULT_FONT_NAME = "Dialog"; private static final String DEFAULT_FONT_NAME = "Dialog";
private static final int DEFAULT_MARGIN_BORDER_SIZE = 4; private static final int DEFAULT_MARGIN_BORDER_SIZE = 8;
private static final float LABEL_TO_ICON_PROPORTION_WAG = 1.4f; private static final float LABEL_TO_ICON_PROPORTION = 1.1f;
private static final double SQRT_2 = Math.sqrt(2.0); private final JLabel rendererLabel = new JLabel();
private JLabel rendererLabel = new JLabel(); private final Map<RenderingHints.Key, Object> renderingHints = new HashMap<>();
private Map<RenderingHints.Key, Object> renderingHints = new HashMap<>();
private int strokeThickness = DEFAULT_STROKE_THICKNESS; private int strokeThickness = DEFAULT_STROKE_THICKNESS;
private Map<AttributedVertex, Icon> map = new ConcurrentHashMap<>(); private final Map<AttributedVertex, Icon> map = new ConcurrentHashMap<>();
private IconShape.Function iconShapeFunction = new IconShape.Function(); private final IconShape.Function iconShapeFunction = new IconShape.Function();
Icon get(AttributedVertex vertex) { Icon get(AttributedVertex vertex) {
@ -64,7 +63,6 @@ public class GhidraIconCache {
private Icon createIcon(AttributedVertex vertex) { private Icon createIcon(AttributedVertex vertex) {
rendererLabel.setText(ProgramGraphFunctions.getLabel(vertex)); rendererLabel.setText(ProgramGraphFunctions.getLabel(vertex));
rendererLabel.setFont(new Font(DEFAULT_FONT_NAME, Font.BOLD, DEFAULT_FONT_SIZE)); rendererLabel.setFont(new Font(DEFAULT_FONT_NAME, Font.BOLD, DEFAULT_FONT_SIZE));
rendererLabel.setForeground(Color.black); rendererLabel.setForeground(Color.black);
rendererLabel.setBackground(Color.white); rendererLabel.setBackground(Color.white);
@ -102,15 +100,15 @@ public class GhidraIconCache {
// triangles have a non-zero +/- yoffset instead of centering the label // triangles have a non-zero +/- yoffset instead of centering the label
case TRIANGLE: case TRIANGLE:
// scale the vertex shape // scale the vertex shape
scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() * LABEL_TO_ICON_PROPORTION_WAG; scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() * LABEL_TO_ICON_PROPORTION;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * LABEL_TO_ICON_PROPORTION_WAG; scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * LABEL_TO_ICON_PROPORTION;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley) vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape); .createTransformedShape(vertexShape);
offset = -(int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2); offset = -(int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2);
break; break;
case INVERTED_TRIANGLE: case INVERTED_TRIANGLE:
scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() * LABEL_TO_ICON_PROPORTION_WAG; scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() * LABEL_TO_ICON_PROPORTION;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * LABEL_TO_ICON_PROPORTION_WAG; scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * LABEL_TO_ICON_PROPORTION;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley) vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape); .createTransformedShape(vertexShape);
offset = (int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2); offset = (int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2);
@ -128,8 +126,8 @@ public class GhidraIconCache {
case DIAMOND: case DIAMOND:
default: // ELLIPSE default: // ELLIPSE
scalex = scalex =
labelSize.getWidth() / vertexShape.getBounds().getWidth() * SQRT_2; labelSize.getWidth() / vertexShape.getBounds().getWidth() * 1.1;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * 2; scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * 1.1;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley) vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape); .createTransformedShape(vertexShape);
break; break;
@ -163,6 +161,21 @@ public class GhidraIconCache {
graphics.setPaint(Color.black); graphics.setPaint(Color.black);
graphics.setTransform(offsetTransform); graphics.setTransform(offsetTransform);
label.paint(graphics); label.paint(graphics);
// draw the shape again, but lighter (on top of the label)
offsetTransform =
AffineTransform.getTranslateInstance(strokeThickness + vertexBounds.width / 2.0,
strokeThickness + vertexBounds.height / 2.0);
offsetTransform.preConcatenate(graphicsTransform);
graphics.setTransform(offsetTransform);
Paint paint = Colors.getColor(vertex);
if (paint instanceof Color) {
Color color = (Color)paint;
Color transparent = new Color(color.getRed(), color.getGreen(), color.getBlue(), 50);
graphics.setPaint(transparent);
graphics.setStroke(new BasicStroke(strokeThickness));
graphics.draw(vertexShape);
}
graphics.setTransform(graphicsTransform); // restore the original transform graphics.setTransform(graphicsTransform); // restore the original transform
graphics.dispose(); graphics.dispose();
return new ImageIcon(bufferedImage); return new ImageIcon(bufferedImage);
@ -171,4 +184,13 @@ public class GhidraIconCache {
public void clear() { public void clear() {
map.clear(); map.clear();
} }
/**
* evict the passed vertex from the cache so that it will be recomputed
* with presumably changed values
* @param vertex to remove from the cache
*/
public void evict(AttributedVertex vertex) {
map.remove(vertex);
}
} }

View File

@ -15,15 +15,21 @@
*/ */
package ghidra.graph.visualization; package ghidra.graph.visualization;
import java.util.function.Function; import org.jungrapht.visualization.layout.algorithms.BalloonLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.CircleLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.*; import org.jungrapht.visualization.layout.algorithms.EiglspergerLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.FRLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.GEMLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.KKLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.RadialTreeLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.TidierRadialTreeLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.TidierTreeLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.TreeLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.repulsion.BarnesHutFRRepulsion;
import org.jungrapht.visualization.layout.algorithms.sugiyama.Layering; import org.jungrapht.visualization.layout.algorithms.sugiyama.Layering;
import org.jungrapht.visualization.layout.algorithms.repulsion.BarnesHutFRRepulsion; import java.util.function.Function;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
/** /**
* A central location to list and provide all layout algorithms, their names, and their builders * A central location to list and provide all layout algorithms, their names, and their builders
@ -32,68 +38,77 @@ import ghidra.service.graph.AttributedVertex;
* This class provides LayoutAlgorithm builders instead of LayoutAlgorithms because some LayoutAlgorithms * This class provides LayoutAlgorithm builders instead of LayoutAlgorithms because some LayoutAlgorithms
* accumulate state information (so are used only one time). * accumulate state information (so are used only one time).
*/ */
class LayoutFunction class LayoutFunction<V, E>
implements Function<String, LayoutAlgorithm.Builder<AttributedVertex, ?, ?>> { implements Function<String, LayoutAlgorithm.Builder<V, ?, ?>> {
static final String KAMADA_KAWAI = "Force Balanced"; static final String KAMADA_KAWAI = "Force Balanced";
static final String FRUCTERMAN_REINGOLD = "Force Directed"; static final String FRUCTERMAN_REINGOLD = "Force Directed";
static final String CIRCLE_MINCROSS = "Circle"; static final String CIRCLE_MINCROSS = "Circle";
static final String TIDIER_TREE = "Compact Hierarchical"; static final String TIDIER_TREE = "Compact Hierarchical";
static final String TIDIER_RADIAL_TREE = "Compact Radial";
static final String MIN_CROSS_TOP_DOWN = "Hierarchical MinCross Top Down"; static final String MIN_CROSS_TOP_DOWN = "Hierarchical MinCross Top Down";
static final String MIN_CROSS_LONGEST_PATH = "Hierarchical MinCross Longest Path"; static final String MIN_CROSS_LONGEST_PATH = "Hierarchical MinCross Longest Path";
static final String MIN_CROSS_NETWORK_SIMPLEX = "Hierarchical MinCross Network Simplex"; static final String MIN_CROSS_NETWORK_SIMPLEX = "Hierarchical MinCross Network Simplex";
static final String MIN_CROSS_COFFMAN_GRAHAM = "Hierarchical MinCross Coffman Graham"; static final String MIN_CROSS_COFFMAN_GRAHAM = "Hierarchical MinCross Coffman Graham";
static final String MULTI_ROW_EDGE_AWARE_TREE = "Hierarchical MultiRow"; static final String TREE = "Hierarchical";
static final String EDGE_AWARE_TREE = "Hierarchical"; static final String RADIAL = "Radial";
static final String EDGE_AWARE_RADIAL = "Radial"; static final String BALLOON = "Balloon";
static final String GEM = "Gem (Graph Embedder)";
public String[] getNames() { public String[] getNames() {
return new String[] { EDGE_AWARE_TREE, MULTI_ROW_EDGE_AWARE_TREE, TIDIER_TREE, return new String[] { TIDIER_TREE, TREE,
MIN_CROSS_TOP_DOWN, MIN_CROSS_LONGEST_PATH, MIN_CROSS_NETWORK_SIMPLEX, TIDIER_RADIAL_TREE, MIN_CROSS_TOP_DOWN, MIN_CROSS_LONGEST_PATH,
MIN_CROSS_COFFMAN_GRAHAM, CIRCLE_MINCROSS, KAMADA_KAWAI, FRUCTERMAN_REINGOLD, MIN_CROSS_NETWORK_SIMPLEX, MIN_CROSS_COFFMAN_GRAHAM, CIRCLE_MINCROSS,
EDGE_AWARE_RADIAL }; KAMADA_KAWAI, FRUCTERMAN_REINGOLD, RADIAL, BALLOON, GEM
};
} }
@Override @Override
public LayoutAlgorithm.Builder<AttributedVertex, ?, ?> apply(String name) { public LayoutAlgorithm.Builder<V, ?, ?> apply(String name) {
switch(name) { switch(name) {
case GEM:
return GEMLayoutAlgorithm.edgeAwareBuilder();
case KAMADA_KAWAI: case KAMADA_KAWAI:
return KKLayoutAlgorithm.<AttributedVertex> builder().preRelaxDuration(1000); return KKLayoutAlgorithm.<V> builder()
.preRelaxDuration(1000);
case FRUCTERMAN_REINGOLD: case FRUCTERMAN_REINGOLD:
return FRLayoutAlgorithm.<AttributedVertex> builder() return FRLayoutAlgorithm.<V> builder()
.repulsionContractBuilder(BarnesHutFRRepulsion.barnesHutBuilder()); .repulsionContractBuilder(BarnesHutFRRepulsion.builder());
case CIRCLE_MINCROSS: case CIRCLE_MINCROSS:
return CircleLayoutAlgorithm.<AttributedVertex> builder() return CircleLayoutAlgorithm.<V> builder()
.reduceEdgeCrossing(true); .reduceEdgeCrossing(true);
case TIDIER_TREE: case TIDIER_RADIAL_TREE:
return TidierTreeLayoutAlgorithm.<AttributedVertex, AttributedEdge> edgeAwareBuilder(); return TidierRadialTreeLayoutAlgorithm.<V, E> edgeAwareBuilder();
case MIN_CROSS_TOP_DOWN: case MIN_CROSS_TOP_DOWN:
return HierarchicalMinCrossLayoutAlgorithm return EiglspergerLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder() .<V, E> edgeAwareBuilder()
.layering(Layering.TOP_DOWN); .layering(Layering.TOP_DOWN);
case MIN_CROSS_LONGEST_PATH: case MIN_CROSS_LONGEST_PATH:
return EiglspergerLayoutAlgorithm return EiglspergerLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder() .<V, E> edgeAwareBuilder()
.layering(Layering.LONGEST_PATH); .layering(Layering.LONGEST_PATH);
case MIN_CROSS_NETWORK_SIMPLEX: case MIN_CROSS_NETWORK_SIMPLEX:
return EiglspergerLayoutAlgorithm return EiglspergerLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder() .<V, E> edgeAwareBuilder()
.layering(Layering.NETWORK_SIMPLEX); .layering(Layering.NETWORK_SIMPLEX);
case MIN_CROSS_COFFMAN_GRAHAM: case MIN_CROSS_COFFMAN_GRAHAM:
return EiglspergerLayoutAlgorithm return EiglspergerLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder() .<V, E> edgeAwareBuilder()
.layering(Layering.COFFMAN_GRAHAM); .layering(Layering.COFFMAN_GRAHAM);
case MULTI_ROW_EDGE_AWARE_TREE: case RADIAL:
return MultiRowEdgeAwareTreeLayoutAlgorithm return RadialTreeLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder(); .<V> builder()
case EDGE_AWARE_RADIAL:
return RadialEdgeAwareTreeLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder()
.verticalVertexSpacing(300); .verticalVertexSpacing(300);
case EDGE_AWARE_TREE: case BALLOON:
return BalloonLayoutAlgorithm
.<V> builder()
.verticalVertexSpacing(300);
case TREE:
return TreeLayoutAlgorithm
.builder();
case TIDIER_TREE:
default: default:
return EdgeAwareTreeLayoutAlgorithm return TidierTreeLayoutAlgorithm.<V, E> edgeAwareBuilder();
.<AttributedVertex, AttributedEdge> edgeAwareBuilder();
} }
} }
} }

View File

@ -15,101 +15,80 @@
*/ */
package ghidra.graph.visualization; package ghidra.graph.visualization;
import static ghidra.graph.visualization.LayoutFunction.*;
import java.awt.Shape;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jgrapht.Graph;
import org.jungrapht.visualization.RenderContext; import org.jungrapht.visualization.RenderContext;
import org.jungrapht.visualization.VisualizationServer; import org.jungrapht.visualization.VisualizationServer;
import org.jungrapht.visualization.layout.algorithms.*; import org.jungrapht.visualization.layout.algorithms.Balloon;
import org.jungrapht.visualization.layout.algorithms.util.*; import org.jungrapht.visualization.layout.algorithms.BalloonLayoutAlgorithm;
import org.jungrapht.visualization.layout.model.LayoutModel; import org.jungrapht.visualization.layout.algorithms.EdgeSorting;
import org.jungrapht.visualization.util.Context; import org.jungrapht.visualization.layout.algorithms.Layered;
import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.RadialTreeLayout;
import org.jungrapht.visualization.layout.algorithms.RadialTreeLayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.TreeLayout;
import org.jungrapht.visualization.layout.algorithms.util.VertexBoundsFunctionConsumer;
import org.jungrapht.visualization.layout.model.Rectangle;
import org.jungrapht.visualization.util.LayoutAlgorithmTransition;
import org.jungrapht.visualization.util.LayoutPaintable;
import docking.menu.MultiActionDockingAction; import java.util.Comparator;
import ghidra.service.graph.*; import java.util.function.Function;
import java.util.function.Predicate;
import static ghidra.graph.visualization.LayoutFunction.TIDIER_TREE;
/** /**
* Manages the selection and transition from one {@link LayoutAlgorithm} to another * Manages the selection and transition from one {@link LayoutAlgorithm} to another
*/ */
class LayoutTransitionManager { class LayoutTransitionManager<V, E> {
LayoutFunction layoutFunction = new LayoutFunction(); LayoutFunction layoutFunction = new LayoutFunction();
/** /**
* the {@link VisualizationServer} used to display graphs using the requested {@link LayoutAlgorithm} * the {@link VisualizationServer} used to display graphs using the requested {@link LayoutAlgorithm}
*/ */
VisualizationServer<AttributedVertex, AttributedEdge> visualizationServer; VisualizationServer<V, E> visualizationServer;
/** /**
* a {@link Predicate} to assist in determining which vertices are root vertices (for Tree layouts) * a {@link Predicate} to assist in determining which vertices are root vertices (for Tree layouts)
*/ */
Predicate<AttributedVertex> rootPredicate; Predicate<V> rootPredicate;
/**
* a {@link Predicate} to allow different handling of specific edge types
*/
Predicate<AttributedEdge> edgePredicate;
/** /**
* a {@link Comparator} to sort edges during layout graph traversal * a {@link Comparator} to sort edges during layout graph traversal
*/ */
Comparator<AttributedEdge> edgeComparator = (e1, e2) -> 0; Comparator<E> edgeComparator = (e1, e2) -> 0;
/** /**
* a {@link MultiActionDockingAction} to allow the user to select a layout algorithm * a {@link Function} to provide {@link Rectangle} (and thus bounds} for vertices
*/ */
MultiActionDockingAction multiActionDockingAction; Function<V, Rectangle> vertexBoundsFunction;
/**
* the currently active {@code LayoutAlgorithm.Builder}
*/
LayoutAlgorithm.Builder<AttributedVertex, ?, ?> activeBuilder;
/**
* a {@link Function} to provide {@link Shape} (and thus bounds} for vertices
*/
Function<AttributedVertex, Shape> vertexShapeFunction;
/** /**
* the {@link RenderContext} used to draw the graph * the {@link RenderContext} used to draw the graph
*/ */
RenderContext<AttributedVertex, AttributedEdge> renderContext; RenderContext<V, E> renderContext;
/** LayoutPaintable.BalloonRings<V, E> balloonLayoutRings;
* a LayoutAlgorithm may change the edge shape function (Sugiyama for articulated edges)
* This is a reference to the original edge shape function so that it can be returned to LayoutPaintable.RadialRings<V> radialLayoutRings;
* the original edge shape function for subsequent LayoutAlgorithm requests
*/
private Function<Context<Graph<AttributedVertex, AttributedEdge>, AttributedEdge>, Shape> originalEdgeShapeFunction;
/** /**
* Create an instance with passed parameters * Create an instance with passed parameters
* @param visualizationServer displays the graph * @param visualizationServer displays the graph
* @param rootPredicate selects root vertices * @param rootPredicate selects root vertices
* @param edgePredicate differentiates edges
*/ */
public LayoutTransitionManager( public LayoutTransitionManager(
VisualizationServer<AttributedVertex, AttributedEdge> visualizationServer, VisualizationServer<V, E> visualizationServer,
Predicate<AttributedVertex> rootPredicate, Predicate<AttributedEdge> edgePredicate) { Predicate<V> rootPredicate) {
this.visualizationServer = visualizationServer; this.visualizationServer = visualizationServer;
this.rootPredicate = rootPredicate; this.rootPredicate = rootPredicate;
this.edgePredicate = edgePredicate;
this.renderContext = visualizationServer.getRenderContext(); this.renderContext = visualizationServer.getRenderContext();
this.vertexShapeFunction = visualizationServer.getRenderContext().getVertexShapeFunction(); this.vertexBoundsFunction = visualizationServer.getRenderContext().getVertexBoundsFunction();
this.originalEdgeShapeFunction =
visualizationServer.getRenderContext().getEdgeShapeFunction();
} }
public void setGraph(AttributedGraph graph) { public void setEdgeComparator(Comparator<E> edgeComparator) {
edgeComparator = new EdgeComparator(graph, "EdgeType", DefaultGraphDisplay.FAVORED_EDGE); this.edgeComparator = edgeComparator;
} }
/** /**
@ -118,93 +97,71 @@ class LayoutTransitionManager {
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void setLayout(String layoutName) { public void setLayout(String layoutName) {
LayoutAlgorithm.Builder<AttributedVertex, ?, ?> builder = layoutFunction.apply(layoutName); LayoutAlgorithm.Builder<V, ?, ?> builder = layoutFunction.apply(layoutName);
visualizationServer.getRenderContext().getMultiLayerTransformer().setToIdentity(); LayoutAlgorithm<V> layoutAlgorithm = builder.build();
LayoutAlgorithm<AttributedVertex> layoutAlgorithm = builder.build(); if (layoutAlgorithm instanceof VertexBoundsFunctionConsumer) {
((VertexBoundsFunctionConsumer<V>) layoutAlgorithm)
if (layoutAlgorithm instanceof RenderContextAware) { .setVertexBoundsFunction(vertexBoundsFunction);
((RenderContextAware<AttributedVertex, AttributedEdge>) layoutAlgorithm)
.setRenderContext(visualizationServer.getRenderContext());
} }
else { if (layoutAlgorithm instanceof Layered) {
visualizationServer.getRenderContext().setEdgeShapeFunction(originalEdgeShapeFunction); ((Layered<V, E>)layoutAlgorithm)
} .setMaxLevelCrossFunction(g ->
if (layoutAlgorithm instanceof VertexShapeAware) { Math.max(1, Math.min(10, 500 / g.vertexSet().size())));
((VertexShapeAware<AttributedVertex>) layoutAlgorithm)
.setVertexShapeFunction(vertexShapeFunction);
} }
if (layoutAlgorithm instanceof TreeLayout) { if (layoutAlgorithm instanceof TreeLayout) {
((TreeLayout<AttributedVertex>) layoutAlgorithm).setRootPredicate(rootPredicate); ((TreeLayout<V>) layoutAlgorithm).setRootPredicate(rootPredicate);
} }
// remove any previously added layout paintables
removePaintable(radialLayoutRings);
removePaintable(balloonLayoutRings);
if (layoutAlgorithm instanceof BalloonLayoutAlgorithm) {
balloonLayoutRings =
new LayoutPaintable.BalloonRings<>(
visualizationServer, (BalloonLayoutAlgorithm<V>) layoutAlgorithm);
visualizationServer.addPreRenderPaintable(balloonLayoutRings);
}
if (layoutAlgorithm instanceof RadialTreeLayout) {
radialLayoutRings =
new LayoutPaintable.RadialRings<>(
visualizationServer, (RadialTreeLayout<V>) layoutAlgorithm);
visualizationServer.addPreRenderPaintable(radialLayoutRings);
}
if (layoutAlgorithm instanceof EdgeSorting) { if (layoutAlgorithm instanceof EdgeSorting) {
((EdgeSorting<AttributedEdge>) layoutAlgorithm).setEdgeComparator(edgeComparator); ((EdgeSorting<E>) layoutAlgorithm).setEdgeComparator(edgeComparator);
} }
if (layoutAlgorithm instanceof EdgePredicated) { LayoutAlgorithmTransition.apply(visualizationServer, layoutAlgorithm);
((EdgePredicated<AttributedEdge>) layoutAlgorithm).setEdgePredicate(edgePredicate); }
private void removePaintable(VisualizationServer.Paintable paintable) {
if (paintable != null) {
visualizationServer.removePreRenderPaintable(paintable);
} }
if (!(layoutAlgorithm instanceof TreeLayout)) {
LayoutModel<AttributedVertex> layoutModel =
visualizationServer.getVisualizationModel().getLayoutModel();
int preferredWidth = layoutModel.getPreferredWidth();
int preferredHeight = layoutModel.getPreferredHeight();
layoutModel.setSize(preferredWidth, preferredHeight);
}
if (layoutAlgorithm instanceof RenderContextAware) {
((RenderContextAware<AttributedVertex, AttributedEdge>) layoutAlgorithm)
.setRenderContext(renderContext);
}
if (layoutAlgorithm instanceof AfterRunnable) {
((AfterRunnable) layoutAlgorithm).setAfter(visualizationServer::scaleToLayout);
}
LayoutAlgorithmTransition.apply(visualizationServer, layoutAlgorithm,
visualizationServer::scaleToLayout);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public LayoutAlgorithm<AttributedVertex> getInitialLayoutAlgorithm( public LayoutAlgorithm<V> getInitialLayoutAlgorithm() {
AttributedGraph graph) { LayoutAlgorithm<V> initialLayoutAlgorithm =
Set<AttributedVertex> roots = getRoots(graph); layoutFunction.apply(TIDIER_TREE).build();
// if there are no roots, don't attempt to create a Tree layout
if (roots.size() == 0) {
return layoutFunction.apply(FRUCTERMAN_REINGOLD).build();
}
LayoutAlgorithm<AttributedVertex> initialLayoutAlgorithm =
layoutFunction.apply(EDGE_AWARE_TREE).build();
if (initialLayoutAlgorithm instanceof TreeLayout) { if (initialLayoutAlgorithm instanceof TreeLayout) {
((TreeLayout<AttributedVertex>) initialLayoutAlgorithm) ((TreeLayout<V>) initialLayoutAlgorithm)
.setRootPredicate(rootPredicate); .setRootPredicate(rootPredicate);
((TreeLayout<AttributedVertex>) initialLayoutAlgorithm) ((TreeLayout<V>) initialLayoutAlgorithm)
.setVertexShapeFunction(vertexShapeFunction); .setVertexBoundsFunction(vertexBoundsFunction);
} }
if (initialLayoutAlgorithm instanceof EdgeSorting) { if (initialLayoutAlgorithm instanceof EdgeSorting) {
((EdgeSorting<AttributedEdge>) initialLayoutAlgorithm) ((EdgeSorting<E>) initialLayoutAlgorithm)
.setEdgeComparator(edgeComparator); .setEdgeComparator(edgeComparator);
} }
if (initialLayoutAlgorithm instanceof EdgePredicated) { if (initialLayoutAlgorithm instanceof VertexBoundsFunctionConsumer) {
((EdgePredicated<AttributedEdge>) initialLayoutAlgorithm) ((VertexBoundsFunctionConsumer<V>) initialLayoutAlgorithm)
.setEdgePredicate(edgePredicate); .setVertexBoundsFunction(vertexBoundsFunction);
}
if (initialLayoutAlgorithm instanceof ShapeFunctionAware) {
((ShapeFunctionAware<AttributedVertex>) initialLayoutAlgorithm)
.setVertexShapeFunction(vertexShapeFunction);
} }
return initialLayoutAlgorithm; return initialLayoutAlgorithm;
} }
private Set<AttributedVertex> getRoots(AttributedGraph graph) {
return graph.edgeSet()
.stream()
.sorted(edgeComparator)
.map(graph::getEdgeSource)
.filter(rootPredicate)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
public String[] getLayoutNames() { public String[] getLayoutNames() {
return layoutFunction.getNames(); return layoutFunction.getNames();
} }
} }

View File

@ -0,0 +1,57 @@
/* ###
* 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 java.awt.BorderLayout;
import javax.swing.*;
import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import docking.DialogComponentProvider;
import ghidra.service.graph.AttributedVertex;
/**
* Extends DialogComponentProvider to make a dialog with buttons to show that the
* layout arrangement algorithm is busy
*/
public class LayoutWorkingDialog extends DialogComponentProvider {
public LayoutWorkingDialog(LayoutAlgorithm<AttributedVertex> layoutAlgorithm) {
super("Working....", false);
super.addWorkPanel(createPanel(layoutAlgorithm));
setRememberSize(false);
addDismissButton();
setDefaultButton(dismissButton);
}
/**
* Create a layout-formatted JComponent holding 2 vertical lists
* of buttons, one list for vertex filter buttons and one list for
* edge filter buttons. Each list has a border and title.
* @return a formatted JComponent (container)
*/
JComponent createPanel(LayoutAlgorithm<AttributedVertex> layoutAlgorithm) {
JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
JPanel panel = new JPanel(new BorderLayout());
panel.add(progressBar, BorderLayout.CENTER);
panel.add(new JLabel("Please wait......."), BorderLayout.NORTH);
addCancelButton();
cancelButton.addActionListener(evt -> layoutAlgorithm.cancel());
return panel;
}
}

View File

@ -0,0 +1,68 @@
/* ###
* 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");
AbstractButton deselectButton = new JMenuItem("Deselect");
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

@ -0,0 +1,68 @@
/* ###
* 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");
AbstractButton deselectButton = new JMenuItem("Deselect");
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

@ -18,6 +18,7 @@ package ghidra.graph.visualization;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import ghidra.service.graph.Attributed; import ghidra.service.graph.Attributed;
import ghidra.service.graph.AttributedEdge; import ghidra.service.graph.AttributedEdge;
import org.apache.commons.text.StringEscapeUtils;
import org.jungrapht.visualization.util.ShapeFactory; import org.jungrapht.visualization.util.ShapeFactory;
import java.awt.BasicStroke; import java.awt.BasicStroke;
@ -129,7 +130,7 @@ abstract class ProgramGraphFunctions {
public static String getLabel(Attributed attributed) { public static String getLabel(Attributed attributed) {
Map<String, String> map = attributed.getAttributeMap(); Map<String, String> map = attributed.getAttributeMap();
if (map.get("Code") != null) { if (map.get("Code") != null) {
String code = map.get("Code"); String code = StringEscapeUtils.escapeHtml4(map.get("Code"));
return "<html>" + String.join("<p>", Splitter.on('\n').split(code)); return "<html>" + String.join("<p>", Splitter.on('\n').split(code));
} }
return map.get("Name"); return map.get("Name");

View File

@ -0,0 +1,228 @@
/* ###
* 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.jgrapht.graph.AsSubgraph;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.selection.MutableSelectedState;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import java.awt.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* {@code PopupMenu to offer vertex selection options. The user can
* <ul>
* <li>hide vertices that are not selected.
* <li>hide vertices that are selected.
* <li>invert the selection.
* <li>'grow' the selected vertex set outwards following outgoing edges.
* <li>'grow' the selected vertex set inwards following incoming edges.
* <li>Create a new graph display consisting of the SubGraph of the selected vertices and their edges.
*/
public class SelectionFilterMenu<V, E> extends JPopupMenu {
/**
* holds the context of graph visualization
*/
private final VisualizationViewer<V, E> visualizationViewer;
/**
* button to extend the selection outwards along outgoing edges
* its a class member so that it can be re-enabled any time the popup is shown
*/
AbstractButton growSelectionOutButton;
/**
* button to extend the selection inwards along incoming edges
* its a class member so that it can be re-enabled any time the popup is shown
*/
AbstractButton growSelectionInButton;
/**
* Create the popup menu and populate with buttons to:
* <ul>
* <li>hide unselected vertices</li>
* <li>hide selected vertices</li>
* <li>Invert the selection</li>
* <li>Grow the selection outwards following outgoing edges</li>
* <li>Grow the selection inwards following incoming edges</li>
* <li>Display the selected vertices only in a new Graph display</li>
* </ul>
* @param visualizationViewer the {@link VisualizationViewer} that holds the context for graph visualization
* @param subgraphConsumer a {@code Consumer} of a {@link Graph} to display in a new tab or window
*/
public SelectionFilterMenu(VisualizationViewer<V, E> visualizationViewer,
Consumer<Graph<V, E>> subgraphConsumer) {
this.visualizationViewer = visualizationViewer;
MutableSelectedState<V> selectedVertexState = visualizationViewer.getSelectedVertexState();
AbstractButton hideUnselectedToggleButton = new JCheckBox("Hide Unselected");
AbstractButton hideSelectedToggleButton = new JCheckBox("Hide Selected");
hideUnselectedToggleButton.addItemListener(evt ->
manageVertexDisplay(hideSelectedToggleButton.isSelected(), hideUnselectedToggleButton.isSelected()));
hideSelectedToggleButton.addItemListener(evt ->
manageVertexDisplay(hideSelectedToggleButton.isSelected(), hideUnselectedToggleButton.isSelected()));
AbstractButton toggleSelectionButton = new JCheckBox("Invert Selection");
toggleSelectionButton.addActionListener(evt -> {
Graph<V, E> graph = visualizationViewer.getVisualizationModel().getGraph();
graph.vertexSet().forEach(v -> {
if (selectedVertexState.isSelected(v)) {
selectedVertexState.deselect(v);
} else {
selectedVertexState.select(v);
}
}
);
visualizationViewer.repaint();
});
this.growSelectionOutButton = new JButton("Grow Selection Outwards");
this.growSelectionInButton = new JButton("Grow Selection Inwards");
this.growSelectionOutButton.addActionListener(evt -> {
growSelection(Graph::outgoingEdgesOf, Graph::getEdgeTarget);
growSelectionInButton.setEnabled(canGrowSelection(Graph::incomingEdgesOf));
growSelectionOutButton.setEnabled(canGrowSelection(Graph::outgoingEdgesOf));
});
this.growSelectionInButton.addActionListener(evt -> {
growSelection(Graph::incomingEdgesOf, Graph::getEdgeSource);
growSelectionInButton.setEnabled(canGrowSelection(Graph::incomingEdgesOf));
growSelectionOutButton.setEnabled(canGrowSelection(Graph::outgoingEdgesOf));
});
JMenuItem subgraphDisplay = new JMenuItem("Display Selected As Graph");
subgraphDisplay.addActionListener(evt -> {
Graph<V, E> graph = visualizationViewer.getVisualizationModel().getGraph();
subgraphConsumer.accept(new AsSubgraph<>(graph, selectedVertexState.getSelected()));
});
add(hideSelectedToggleButton);
add(hideUnselectedToggleButton);
add(toggleSelectionButton);
add(growSelectionInButton);
add(growSelectionOutButton);
add(subgraphDisplay);
}
private boolean canGrowSelection(BiFunction<Graph<V, E>, V, Set<E>> growthFunction) {
Graph<V, E> graph = visualizationViewer.getVisualizationModel().getGraph();
Set<V> selectedVertices = visualizationViewer.getSelectedVertexState().getSelected();
Set<E> selectedEdges = visualizationViewer.getSelectedEdgeState().getSelected();
return selectedVertices.stream()
.map(v -> growthFunction.apply(graph, v))
.anyMatch(adjacentEdges -> !selectedEdges.containsAll(adjacentEdges));
}
/**
* re-enaable the grow buttons in case the user cleared the selection in the graph display
* @param invoker
* @param x
* @param y
*/
@Override
public void show(Component invoker, int x, int y) {
this.growSelectionInButton.setEnabled(true);
this.growSelectionOutButton.setEnabled(true);
super.show(invoker, x, y);
}
/**
* Use the supplied boolean flags 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>
* @param hideSelected a {@code boolean} flag to request hiding of selected vertices
* @param hideUnselected a {@code boolean} flag to request hiding of unselected vertices
*/
private void manageVertexDisplay(boolean hideSelected, boolean hideUnselected) {
MutableSelectedState<V> selectedVertexState = visualizationViewer.getSelectedVertexState();
if (hideSelected && hideUnselected) {
visualizationViewer.getRenderContext()
.setVertexIncludePredicate(v -> false);
} else if (hideSelected) {
visualizationViewer.getRenderContext()
.setVertexIncludePredicate(Predicate.not(selectedVertexState::isSelected));
} else if (hideUnselected) {
visualizationViewer.getRenderContext()
.setVertexIncludePredicate(selectedVertexState::isSelected);
} else {
visualizationViewer.getRenderContext()
.setVertexIncludePredicate(v -> true);
}
visualizationViewer.repaint();
}
/**
* select all vertices that are one hop away from any currently selected vertices
* @param growthFunction either outgoing or incoming edges
* @param neighborFunction either target or source vertices
* @return true if the selection has changed
*/
private boolean growSelection(BiFunction<Graph<V, E>, V, Set<E>> growthFunction,
BiFunction<Graph<V, E>, E, V> neighborFunction) {
MutableSelectedState<V> selectedVertexState = visualizationViewer.getSelectedVertexState();
Set<V> selectedVertices = new HashSet<>(selectedVertexState.getSelected());
selectedVertexState.getSelected()
.forEach(v -> selectedVertices.addAll(growSelection(v, growthFunction, neighborFunction)));
return selectedVertexState.select(selectedVertices);
}
/**
* select all vertices that are one hop away from the supplied vertex. The outgoing ar
* incoming edges are not followed if they are already selected, or if they are hidden
* by the edgeIncludePredicate. Likewise
* @param vertex the vertex to start the selection from
* @param growthFunction either outgoing or incoming edges
* @param neighborFunction either target or source vertices
* @return a collection of selected vertices
*/
private Collection<V> growSelection(V vertex,
BiFunction<Graph<V, E>, V, Set<E>> growthFunction,
BiFunction<Graph<V, E>, E, V> neighborFunction) {
Graph<V, E> graph = visualizationViewer.getVisualizationModel().getGraph();
MutableSelectedState<V> selectedVertexState = visualizationViewer.getSelectedVertexState();
MutableSelectedState<E> selectedEdgeState = visualizationViewer.getSelectedEdgeState();
Predicate<E> edgeIncludePredicate = visualizationViewer.getRenderContext().getEdgeIncludePredicate();
Predicate<V> vertexIncludePredicate = visualizationViewer.getRenderContext().getVertexIncludePredicate();
// filter out edges we have already selected
Set<E> connectingEdges = growthFunction.apply(graph, vertex)
.stream().filter(e -> !selectedEdgeState.isSelected(e))
.filter(edgeIncludePredicate)
.collect(Collectors.toSet());
visualizationViewer.getSelectedEdgeState().select(connectingEdges);
// get the opposite endpoints for each edge and select them, if they are not already selected
return connectingEdges.stream().map(e -> neighborFunction.apply(graph, e))
.filter(v -> !selectedVertexState.isSelected(v))
.filter(vertexIncludePredicate)
.collect(Collectors.toSet());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

View File

@ -10,10 +10,11 @@ jungrapht.modalRendererTimerSleep=30
# whether the satellite view is drawn with a transparent background # whether the satellite view is drawn with a transparent background
jungrapht.satelliteBackgroundTransparent=false jungrapht.satelliteBackgroundTransparent=false
jungrapht.satelliteLensColor= 0xFAFAFA
# default spacing for tree layouts # default spacing for tree layouts
jungrapht.treeLayoutHorizontalSpacing=400 jungrapht.treeLayoutHorizontalSpacing=2
jungrapht.treeLayoutVerticalSpacing=300 jungrapht.treeLayoutVerticalSpacing=2
# default area of pick footprint (item is picked when footprint intersects item shape) # default area of pick footprint (item is picked when footprint intersects item shape)
jungrapht.pickAreaSize=20 jungrapht.pickAreaSize=20
@ -26,14 +27,14 @@ jungrapht.edgeWidth=4.0f
# stroke size for the magnifier lens # stroke size for the magnifier lens
jungrapht.lensStrokeWidth=10.0 jungrapht.lensStrokeWidth=10.0
# when scale is < .3, switch to lightweight rendering # when scale is < .1, switch to lightweight rendering
jungrapht.lightweightScaleThreshold=.8 jungrapht.lightweightScaleThreshold=.1
# under 50 vertices will use heavyweight rendering all the time # under 50 vertices will use heavyweight rendering all the time
jungrapht.lightweightCountThreshold=50 jungrapht.lightweightCountThreshold=50
# default pixels spacings for vertices # default pixels spacings for vertices
jungrapht.mincross.horizontalOffset=100 jungrapht.mincross.horizontalOffset=10
jungrapht.mincross.verticalOffset=50 jungrapht.mincross.verticalOffset=5
# how many times to run the full all-level cross count # how many times to run the full all-level cross count
jungrapht.mincross.maxLevelCross=10 jungrapht.mincross.maxLevelCross=10
@ -50,4 +51,7 @@ jungrapht.mincross.eiglspergerThreshold=200
# over 200 edges and the reduce edge crossing algorithm will not run # over 200 edges and the reduce edge crossing algorithm will not run
jungrapht.circle.reduceEdgeCrossingMaxEdges=200 jungrapht.circle.reduceEdgeCrossingMaxEdges=200
# density of a graph in layout. (non-zero) values are 0 < density <= 1.0
# for layout algorithms that attempt to make the area optimal for the number
# of graph vertices, this sets how dense the space is.
jungrapht.initialDimensionVertexDensity=0.3f

View File

@ -35,6 +35,7 @@ public class MultiStateActionBuilder<T> extends
private BiConsumer<ActionState<T>, EventTrigger> actionStateChangedCallback; private BiConsumer<ActionState<T>, EventTrigger> actionStateChangedCallback;
private boolean performActionOnButtonClick = false; private boolean performActionOnButtonClick = false;
private boolean fireFirstAction = true;
private List<ActionState<T>> states = new ArrayList<>(); private List<ActionState<T>> states = new ArrayList<>();
@ -114,6 +115,17 @@ public class MultiStateActionBuilder<T> extends
return self(); return self();
} }
/**
* controls whether the first action added will automatically fire an event or not
*
* @param fireFirstAction do fire an action on the first action. Defaults to {@code true}
* @return this MultiActionDockingActionBuilder (for chaining)
*/
public MultiStateActionBuilder<T> fireFirstAction(boolean fireFirstAction) {
this.fireFirstAction = fireFirstAction;
return self();
}
@Override @Override
public MultiStateDockingAction<T> build() { public MultiStateDockingAction<T> build() {
validate(); validate();
@ -133,6 +145,7 @@ public class MultiStateActionBuilder<T> extends
} }
} }
}; };
action.setFireFirstEvent(fireFirstAction);
for (ActionState<T> actionState : states) { for (ActionState<T> actionState : states) {
action.addActionState(actionState); action.addActionState(actionState);

View File

@ -53,6 +53,7 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
private int currentStateIndex = 0; private int currentStateIndex = 0;
private MultiActionDockingActionIf multiActionGenerator; private MultiActionDockingActionIf multiActionGenerator;
private MultipleActionDockingToolbarButton multipleButton; private MultipleActionDockingToolbarButton multipleButton;
private boolean fireFirstEvent = true;
private boolean performActionOnPrimaryButtonClick = true; private boolean performActionOnPrimaryButtonClick = true;
@ -136,6 +137,21 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
doActionPerformed(context); doActionPerformed(context);
} }
/**
* @return {@code true} if the first action automatically fire its event
*/
public boolean isFireFirstEvent() {
return fireFirstEvent;
}
/**
* set the flag to fire an event on the first action
* @param fireFirstEvent whether to fire the event
*/
public void setFireFirstEvent(boolean fireFirstEvent) {
this.fireFirstEvent = fireFirstEvent;
}
/** /**
* This is the callback to be overridden when the child wishes to respond to user button * This is the callback to be overridden when the child wishes to respond to user button
* presses that are on the button and not the drop-down. This will only be called if * presses that are on the button and not the drop-down. This will only be called if
@ -176,9 +192,14 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
tbd.setToolBarSubGroup(subGroup); tbd.setToolBarSubGroup(subGroup);
} }
/**
* add the supplied {@code ActionState}
* if {@code fireFirstEvent} is {@code true} the first one will fire its event
* @param actionState the {@code ActionState} to add
*/
public void addActionState(ActionState<T> actionState) { public void addActionState(ActionState<T> actionState) {
actionStates.add(actionState); actionStates.add(actionState);
if (actionStates.size() == 1) { if (actionStates.size() == 1 && fireFirstEvent) {
setCurrentActionState(actionState); setCurrentActionState(actionState);
} }
} }
@ -188,7 +209,9 @@ public abstract class MultiStateDockingAction<T> extends DockingAction {
throw new IllegalArgumentException("You must provide at least one ActionState"); throw new IllegalArgumentException("You must provide at least one ActionState");
} }
actionStates = new ArrayList<>(newStates); actionStates = new ArrayList<>(newStates);
setCurrentActionState(actionStates.get(0)); if (fireFirstEvent) {
setCurrentActionState(actionStates.get(0));
}
} }
public T getCurrentUserData() { public T getCurrentUserData() {

View File

@ -73,9 +73,11 @@
<logger name="ghidra.app.util.opinion" level="DEBUG" /> <logger name="ghidra.app.util.opinion" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" /> <logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" /> <logger name="ghidra.util.task" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
<Root level="ALL"> <Root level="ALL">
<AppenderRef ref="console" level="DEBUG"/> <AppenderRef ref="console" level="DEBUG"/>
<AppenderRef ref="detail" level="DEBUG"/> <AppenderRef ref="detail" level="DEBUG"/>
<AppenderRef ref="script" level="DEBUG"/> <AppenderRef ref="script" level="DEBUG"/>
<AppenderRef ref="logPanel" level="INFO"/> <AppenderRef ref="logPanel" level="INFO"/>

View File

@ -15,6 +15,8 @@
*/ */
package ghidra.service.graph; package ghidra.service.graph;
import org.apache.commons.text.StringEscapeUtils;
import java.util.Map; import java.util.Map;
/** /**
@ -74,6 +76,10 @@ public class AttributedVertex extends Attributed {
return getName() + " (" + id + ")"; return getName() + " (" + id + ")";
} }
public void clearCache() {
this.htmlString = null;
}
/** /**
* parse (one time) then cache the attributes to html * parse (one time) then cache the attributes to html
* @return the html string * @return the html string
@ -84,7 +90,7 @@ public class AttributedVertex extends Attributed {
for (Map.Entry<String, String> entry : entrySet()) { for (Map.Entry<String, String> entry : entrySet()) {
buf.append(entry.getKey()); buf.append(entry.getKey());
buf.append(":"); buf.append(":");
buf.append(entry.getValue()); buf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
buf.append("<br>"); buf.append("<br>");
} }
htmlString = buf.toString(); htmlString = buf.toString();

View File

@ -38,4 +38,9 @@ public interface GraphDisplayListener {
* @param vertexId the vertex id of the currently "focused" vertex * @param vertexId the vertex id of the currently "focused" vertex
*/ */
public void locationChanged(String vertexId); public void locationChanged(String vertexId);
default boolean updateVertexName(String vertexId, String oldName, String newName) {
// no op
return false;
}
} }