mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-10-13 13:43:00 +00:00
Merge remote-tracking branch 'origin/program_graph_updates2--SQUASHED'
This commit is contained in:
commit
f84300819b
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -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.*
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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) ||
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"))
|
||||||
);
|
);
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
|
|
@ -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 |
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -73,6 +73,8 @@
|
||||||
<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"/>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue