Merge remote-tracking branch 'origin/GP-773_ghidravore_graph_visualization_options--SQUASHED'

This commit is contained in:
Ryan Kurtz 2021-08-09 14:13:19 -04:00
commit 69e8119211
84 changed files with 4102 additions and 1822 deletions

View file

@ -248,6 +248,41 @@
</UL>
</BLOCKQUOTE>
<H2><A name="Graph_Type_Display_Options">Graph Type Display Options</H2>
<BLOCKQUOTE>
<P>Graphs have a graph type which defines vertex types and edge types. Users can
configure the display properties for each vertex and edge type. These options have the
following subsections:</P>
<H3>Edge Colors</H3>
<BLOCKQUOTE>
<P>Allows setting the color for each edge type. Each Edge type will be listed with its
current color.</P>
</BLOCKQUOTE>
<H3>Miscellaneous</H3>
<UL>
<LI>Default Vertex Color - color for vertices with no defined vertex type</LI>
<LI>Default Vertex Shape - shape for vertices with no defined vertex type</LI>
<LI>Default Edge Color - color for edges with no defined edge type</LI>
<LI>Favored Edge - edge type to be favored by graph layout algorithms</LI>
</UL>
<H3>Vertex Colors</H3>
<BLOCKQUOTE>
<P>Allows setting the color for each vertex type. Each vertex type will be listed with
its current color.</P>
</BLOCKQUOTE>
<H3>Vertex Shapes</H3>
<BLOCKQUOTE>
<P>Allows setting the shape for each vertex type. Each vertex type will be listed with a
combo box for picking a supported shape. Supported shapes include Ellipse, Rectangle
Diamond, TriangleUp, TriangleDown, Star, Pentagon, Hexagon, and Octagon.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided By:&nbsp; <I>GraphDisplayBrokerPlugin</I></P>
<P class="relatedtopic">Related Topics:</P>

View file

@ -155,7 +155,7 @@ public class AttributeFilters implements ItemSelectable {
// count up the unique attribute values (skipping the 'precluded names' we know we don't want)
for (Attributed element : elements) {
Map<String, String> attributeMap = new HashMap<>(element.getAttributeMap());
Map<String, String> attributeMap = new HashMap<>(element.getAttributes());
for (Map.Entry<String, String> entry : attributeMap.entrySet()) {
if (!precludedNames.contains(entry.getKey())) {
multiset.add(entry.getValue());

View file

@ -24,6 +24,7 @@ import docking.widgets.EventTrigger;
import ghidra.app.services.GraphDisplayBroker;
import ghidra.framework.plugintool.PluginTool;
import ghidra.service.graph.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
@ -77,23 +78,6 @@ class ExportAttributedGraphDisplay implements GraphDisplay {
return Collections.emptyList();
}
@Override
public void defineVertexAttribute(String attributeName) {
// no effect
}
@Override
public void defineEdgeAttribute(String attributeName) {
// no effect
}
@Override
public void setVertexLabelAttribute(String attributeName, int alignment, int size,
boolean monospace,
int maxLines) {
// no effect
}
@Override
public void setGraph(AttributedGraph graph, String title, boolean append,
TaskMonitor monitor) {
@ -102,6 +86,12 @@ class ExportAttributedGraphDisplay implements GraphDisplay {
doSetGraphData(graph);
}
@Override
public void setGraph(AttributedGraph graph, GraphDisplayOptions options, String title,
boolean append, TaskMonitor monitor) throws CancelledException {
this.setGraph(graph, title, append, monitor);
}
/**
* remove all vertices and edges from the {@link Graph}
*/
@ -149,5 +139,4 @@ class ExportAttributedGraphDisplay implements GraphDisplay {
public void selectVertices(Set<AttributedVertex> vertexList, EventTrigger eventTrigger) {
// not interactive, so N/A
}
}

View file

@ -50,13 +50,13 @@ public class GraphMlGraphExporter extends AbstractAttributedGraphExporter {
entry -> new DefaultAttribute<>(entry.getValue(), AttributeType.STRING))));
graph.vertexSet().stream()
.map(Attributed::getAttributeMap)
.map(Attributed::getAttributes)
.flatMap(m -> m.entrySet().stream())
.map(Map.Entry::getKey)
.forEach(key -> exporter.registerAttribute(key, GraphMLExporter.AttributeCategory.NODE, AttributeType.STRING));
graph.edgeSet().stream()
.map(Attributed::getAttributeMap)
.map(Attributed::getAttributes)
.flatMap(m -> m.entrySet().stream())
.map(Map.Entry::getKey)
.forEach(key -> exporter.registerAttribute(key, GraphMLExporter.AttributeCategory.EDGE, AttributeType.STRING));

View file

@ -0,0 +1,137 @@
/* ###
* 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.event.MouseEvent;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JToolTip;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import com.google.common.base.Splitter;
import ghidra.graph.viewer.popup.ToolTipInfo;
import ghidra.service.graph.*;
/**
* Generates tool tips for an {@link AttributedVertex} or {@link AttributedEdge} in
* an {@link AttributedGraph}
*/
public class AttributedToolTipInfo extends ToolTipInfo<Attributed> {
AttributedToolTipInfo(Attributed graphObject, MouseEvent event) {
super(event, graphObject);
}
@Override
public JComponent createToolTipComponent() {
if (graphObject == null) {
return null;
}
String toolTip = getToolTipText();
if (StringUtils.isBlank(toolTip)) {
return null;
}
JToolTip jToolTip = new JToolTip();
jToolTip.setTipText(toolTip);
return jToolTip;
}
@Override
protected void emphasize() {
// this graph display does not have a notion of emphasizing
}
@Override
protected void deEmphasize() {
// this graph display does not have a notion of emphasizing
}
/**
* Returns the tool tip for the graphObject this object manages
* @return the tool tip for the graphObject this object manages
*/
public String getToolTipText() {
String tooltipText = graphObject.getDescription();
if (tooltipText != null) {
return tooltipText;
}
StringBuilder buf = new StringBuilder();
buf.append("<HTML>");
if (graphObject instanceof AttributedVertex) {
addToolTipTextForVertex(buf, (AttributedVertex) graphObject);
}
else if (graphObject instanceof AttributedEdge) {
addToolTipTextForEdge(buf, (AttributedEdge) graphObject);
}
return buf.toString();
}
private void addToolTipTextForVertex(StringBuilder buf, AttributedVertex vertex) {
String vertexType = vertex.getVertexType();
buf.append("<H4>");
buf.append(vertex.getName());
if (vertexType != null) {
buf.append("<br>");
buf.append("Type: &nbsp;" + vertexType);
}
buf.append("</H4>");
addAttributes(buf, AttributedVertex.NAME_KEY, AttributedVertex.VERTEX_TYPE_KEY);
}
private void addToolTipTextForEdge(StringBuilder buf, AttributedEdge edge) {
String edgeType = edge.getEdgeType();
if (edgeType != null) {
buf.append("<H4>");
buf.append("Type: &nbsp;" + edgeType);
buf.append("</H4>");
}
addAttributes(buf, AttributedEdge.EDGE_TYPE_KEY);
}
private void addAttributes(StringBuilder buf, String...excludedKeys) {
Set<Entry<String, String>> entries = graphObject.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (ArrayUtils.contains(excludedKeys, key)) {
continue; // skip keys handled in header
}
buf.append(key);
buf.append(": ");
String value = entry.getValue();
value = StringEscapeUtils.escapeHtml4(value);
String split = String.join("<br>", Splitter.on('\n').split(value));
split = split.replaceAll("\\s", "&nbsp;");
buf.append(split);
buf.append("<br>");
}
}
}

View file

@ -1,288 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import static java.util.Map.*;
import java.awt.Color;
import java.awt.Paint;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ghidra.service.graph.Attributed;
/**
* support for coercing colors from attributes or color names
*/
public abstract class Colors {
private static final Pattern HEX_PATTERN = Pattern.compile("(0x|#)[0-9A-Fa-f]{6}");
// cannot instantiate nor extend
private Colors() {
}
/**
* a map of well-known 'web' color names to colors
*/
static Map<String, Color> WEB_COLOR_MAP = Map.ofEntries(
entry("Black", Color.decode("0x000000")),
entry("Navy", Color.decode("0x000080")),
entry("DarkBlue", Color.decode("0x00008B")),
entry("MediumBlue", Color.decode("0x0000CD")),
entry("Blue", Color.decode("0x0000FF")),
entry("DarkGreen", Color.decode("0x006400")),
entry("Green", Color.decode("0x008000")),
entry("Teal", Color.decode("0x008080")),
entry("DarkCyan", Color.decode("0x008B8B")),
entry("DeepSkyBlue", Color.decode("0x00BFFF")),
entry("DarkTurquoise", Color.decode("0x00CED1")),
entry("MediumSpringGreen", Color.decode("0x00FA9A")),
entry("Lime", Color.decode("0x00FF00")),
entry("SpringGreen", Color.decode("0x00FF7F")),
entry("Aqua", Color.decode("0x00FFFF")),
entry("Cyan", Color.decode("0x00FFFF")),
entry("MidnightBlue", Color.decode("0x191970")),
entry("DodgerBlue", Color.decode("0x1E90FF")),
entry("LightSeaGreen", Color.decode("0x20B2AA")),
entry("ForestGreen", Color.decode("0x228B22")),
entry("SeaGreen", Color.decode("0x2E8B57")),
entry("DarkSlateGray", Color.decode("0x2F4F4F")),
entry("DarkSlateGrey", Color.decode("0x2F4F4F")),
entry("LimeGreen", Color.decode("0x32CD32")),
entry("MediumSeaGreen", Color.decode("0x3CB371")),
entry("Turquoise", Color.decode("0x40E0D0")),
entry("RoyalBlue", Color.decode("0x4169E1")),
entry("SteelBlue", Color.decode("0x4682B4")),
entry("DarkSlateBlue", Color.decode("0x483D8B")),
entry("MediumTurquoise", Color.decode("0x48D1CC")),
entry("Indigo", Color.decode("0x4B0082")),
entry("DarkOliveGreen", Color.decode("0x556B2F")),
entry("CadetBlue", Color.decode("0x5F9EA0")),
entry("CornflowerBlue", Color.decode("0x6495ED")),
entry("RebeccaPurple", Color.decode("0x663399")),
entry("MediumAquaMarine", Color.decode("0x66CDAA")),
entry("DimGray", Color.decode("0x696969")),
entry("DimGrey", Color.decode("0x696969")),
entry("SlateBlue", Color.decode("0x6A5ACD")),
entry("OliveDrab", Color.decode("0x6B8E23")),
entry("SlateGray", Color.decode("0x708090")),
entry("SlateGrey", Color.decode("0x708090")),
entry("LightSlateGray", Color.decode("0x778899")),
entry("LightSlateGrey", Color.decode("0x778899")),
entry("MediumSlateBlue", Color.decode("0x7B68EE")),
entry("LawnGreen", Color.decode("0x7CFC00")),
entry("Chartreuse", Color.decode("0x7FFF00")),
entry("Aquamarine", Color.decode("0x7FFFD4")),
entry("Maroon", Color.decode("0x800000")),
entry("Purple", Color.decode("0x800080")),
entry("Olive", Color.decode("0x808000")),
entry("Gray", Color.decode("0x808080")),
entry("Grey", Color.decode("0x808080")),
entry("SkyBlue", Color.decode("0x87CEEB")),
entry("LightSkyBlue", Color.decode("0x87CEFA")),
entry("BlueViolet", Color.decode("0x8A2BE2")),
entry("DarkRed", Color.decode("0x8B0000")),
entry("DarkMagenta", Color.decode("0x8B008B")),
entry("SaddleBrown", Color.decode("0x8B4513")),
entry("DarkSeaGreen", Color.decode("0x8FBC8F")),
entry("LightGreen", Color.decode("0x90EE90")),
entry("MediumPurple", Color.decode("0x9370DB")),
entry("DarkViolet", Color.decode("0x9400D3")),
entry("PaleGreen", Color.decode("0x98FB98")),
entry("DarkOrchid", Color.decode("0x9932CC")),
entry("YellowGreen", Color.decode("0x9ACD32")),
entry("Sienna", Color.decode("0xA0522D")),
entry("Brown", Color.decode("0xA52A2A")),
entry("DarkGray", Color.decode("0xA9A9A9")),
entry("DarkGrey", Color.decode("0xA9A9A9")),
entry("LightBlue", Color.decode("0xADD8E6")),
entry("GreenYellow", Color.decode("0xADFF2F")),
entry("PaleTurquoise", Color.decode("0xAFEEEE")),
entry("LightSteelBlue", Color.decode("0xB0C4DE")),
entry("PowderBlue", Color.decode("0xB0E0E6")),
entry("FireBrick", Color.decode("0xB22222")),
entry("DarkGoldenRod", Color.decode("0xB8860B")),
entry("MediumOrchid", Color.decode("0xBA55D3")),
entry("RosyBrown", Color.decode("0xBC8F8F")),
entry("DarkKhaki", Color.decode("0xBDB76B")),
entry("Silver", Color.decode("0xC0C0C0")),
entry("MediumVioletRed", Color.decode("0xC71585")),
entry("IndianRed", Color.decode("0xCD5C5C")),
entry("Peru", Color.decode("0xCD853F")),
entry("Chocolate", Color.decode("0xD2691E")),
entry("Tan", Color.decode("0xD2B48C")),
entry("LightGray", Color.decode("0xD3D3D3")),
entry("LightGrey", Color.decode("0xD3D3D3")),
entry("Thistle", Color.decode("0xD8BFD8")),
entry("Orchid", Color.decode("0xDA70D6")),
entry("GoldenRod", Color.decode("0xDAA520")),
entry("PaleVioletRed", Color.decode("0xDB7093")),
entry("Crimson", Color.decode("0xDC143C")),
entry("Gainsboro", Color.decode("0xDCDCDC")),
entry("Plum", Color.decode("0xDDA0DD")),
entry("BurlyWood", Color.decode("0xDEB887")),
entry("LightCyan", Color.decode("0xE0FFFF")),
entry("Lavender", Color.decode("0xE6E6FA")),
entry("DarkSalmon", Color.decode("0xE9967A")),
entry("Violet", Color.decode("0xEE82EE")),
entry("PaleGoldenRod", Color.decode("0xEEE8AA")),
entry("LightCoral", Color.decode("0xF08080")),
entry("Khaki", Color.decode("0xF0E68C")),
entry("AliceBlue", Color.decode("0xF0F8FF")),
entry("HoneyDew", Color.decode("0xF0FFF0")),
entry("Azure", Color.decode("0xF0FFFF")),
entry("SandyBrown", Color.decode("0xF4A460")),
entry("Wheat", Color.decode("0xF5DEB3")),
entry("Beige", Color.decode("0xF5F5DC")),
entry("WhiteSmoke", Color.decode("0xF5F5F5")),
entry("MintCream", Color.decode("0xF5FFFA")),
entry("GhostWhite", Color.decode("0xF8F8FF")),
entry("Salmon", Color.decode("0xFA8072")),
entry("AntiqueWhite", Color.decode("0xFAEBD7")),
entry("Linen", Color.decode("0xFAF0E6")),
entry("LightGoldenRodYellow", Color.decode("0xFAFAD2")),
entry("OldLace", Color.decode("0xFDF5E6")),
entry("Red", Color.decode("0xFF0000")),
entry("Fuchsia", Color.decode("0xFF00FF")),
entry("Magenta", Color.decode("0xFF00FF")),
entry("DeepPink", Color.decode("0xFF1493")),
entry("OrangeRed", Color.decode("0xFF4500")),
entry("Tomato", Color.decode("0xFF6347")),
entry("HotPink", Color.decode("0xFF69B4")),
entry("Coral", Color.decode("0xFF7F50")),
entry("DarkOrange", Color.decode("0xFF8C00")),
entry("LightSalmon", Color.decode("0xFFA07A")),
entry("Orange", Color.decode("0xFFA500")),
entry("LightPink", Color.decode("0xFFB6C1")),
entry("Pink", Color.decode("0xFFC0CB")),
entry("Gold", Color.decode("0xFFD700")),
entry("PeachPuff", Color.decode("0xFFDAB9")),
entry("NavajoWhite", Color.decode("0xFFDEAD")),
entry("Moccasin", Color.decode("0xFFE4B5")),
entry("Bisque", Color.decode("0xFFE4C4")),
entry("MistyRose", Color.decode("0xFFE4E1")),
entry("BlanchedAlmond", Color.decode("0xFFEBCD")),
entry("PapayaWhip", Color.decode("0xFFEFD5")),
entry("LavenderBlush", Color.decode("0xFFF0F5")),
entry("SeaShell", Color.decode("0xFFF5EE")),
entry("Cornsilk", Color.decode("0xFFF8DC")),
entry("LemonChiffon", Color.decode("0xFFFACD")),
entry("FloralWhite", Color.decode("0xFFFAF0")),
entry("Snow", Color.decode("0xFFFAFA")),
entry("Yellow", Color.decode("0xFFFF00")),
entry("LightYellow", Color.decode("0xFFFFE0")),
entry("Ivory", Color.decode("0xFFFFF0")),
entry("White", Color.decode("0xFFFFFF"))
);
/**
* a blue that is not as dark as {@code Color.blue}
*/
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
* (the keys are the property values for the vertex/edge keys:
* VertexType and EdgeType)
*/
public static Map<String,Paint> VERTEX_TYPE_TO_COLOR_MAP =
Map.ofEntries(
entry("Body", blue),
entry("Entry", WEB_COLOR_MAP.get("DarkOrange")),
entry("Exit", Color.magenta),
entry("Switch", Color.cyan),
entry("Bad",Color.red),
entry("Entry-Nexus",Color.white),
entry("External",Color.green),
entry("Folder",WEB_COLOR_MAP.get("DarkOrange")),
entry("Fragment",WEB_COLOR_MAP.get("Purple")),
entry("Data",Color.pink)
);
/**
* these are vertex or edge types that have defined colors
* (the keys are the property values for the vertex/edge keys:
* VertexType and EdgeType)
*/
public static Map<String,Paint> EDGE_TYPE_TO_COLOR_MAP =
Map.ofEntries(
entry("Entry", Color.gray), // white??
entry("Fall-Through", Color.blue),
entry("Conditional-Call", WEB_COLOR_MAP.get("DarkOrange")),
entry("Unconditional-Call", WEB_COLOR_MAP.get("DarkOrange")),
entry("Computed",Color.cyan),
entry("Indirection",Color.pink),
entry("Unconditional-Jump", Color.green),
entry("Conditional-Jump", darkerYellow),
entry("Terminator", WEB_COLOR_MAP.get("Purple")),
entry("Conditional-Return", WEB_COLOR_MAP.get("Purple"))
);
/**
* Determine a color for the given {@link Attributed} object.
* <P>
* The attributed object can be an vertex or an edge. This method examines the attributes
* and tries to find an attribute that has a color mapping. Otherwise it returns a default
* color
* @param attributed the vertex or edge for which to determine a color
* @return the color to paint the given Attributed
*/
public static Paint getColor(Attributed attributed) {
Map<String, String> map = attributed.getAttributeMap();
// if there is a 'VertexType' attribute key, use its value to choose a predefined color
if (map.containsKey("VertexType")) {
String typeValue = map.get("VertexType");
return VERTEX_TYPE_TO_COLOR_MAP.getOrDefault(typeValue, Color.blue);
}
// if there is an 'EdgeType' attribute key, use its value to choose a predefined color
if (map.containsKey("EdgeType")) {
String typeValue = map.get("EdgeType");
return EDGE_TYPE_TO_COLOR_MAP.getOrDefault(typeValue, Color.green);
}
// if there is a 'Color' attribute key, use its value (either a color name or an RGB hex string)
// to choose a color
if (map.containsKey("Color")) {
String colorName = map.get("Color");
if (WEB_COLOR_MAP.containsKey(colorName)) {
return WEB_COLOR_MAP.get(colorName);
}
// if the value matches an RGB hex string, turn that into a color
Color c = getHexColor(colorName);
if (c != null) {
return c;
}
}
// default value when nothing else matches
return Color.green;
}
public static Color getHexColor(String hexString) {
Matcher matcher = HEX_PATTERN.matcher(hexString);
if (matcher.matches()) {
return Color.decode(hexString);
}
return null;
}
}

View file

@ -16,46 +16,32 @@
package ghidra.graph.visualization;
import static org.jungrapht.visualization.MultiLayerTransformer.Layer.*;
import static org.jungrapht.visualization.renderers.BiModalRenderer.*;
import java.awt.*;
import java.awt.Dimension;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.*;
import org.apache.commons.lang3.StringUtils;
import org.jgrapht.Graph;
import org.jgrapht.graph.AsSubgraph;
import org.jungrapht.visualization.*;
import org.jungrapht.visualization.annotations.MultiSelectedVertexPaintable;
import org.jungrapht.visualization.annotations.SelectedEdgePaintable;
import org.jungrapht.visualization.annotations.SingleSelectedVertexPaintable;
import org.jungrapht.visualization.annotations.*;
import org.jungrapht.visualization.control.*;
import org.jungrapht.visualization.decorators.*;
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.layout.model.Rectangle;
import org.jungrapht.visualization.renderers.*;
import org.jungrapht.visualization.renderers.Renderer;
import org.jungrapht.visualization.renderers.Renderer.VertexLabel;
import org.jungrapht.visualization.renderers.Renderer.VertexLabel.Position;
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 org.jungrapht.visualization.util.RectangleUtils;
import docking.ActionContext;
import docking.DockingActionProxy;
@ -63,10 +49,15 @@ import docking.action.DockingActionIf;
import docking.action.ToggleDockingAction;
import docking.action.builder.*;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.options.editor.OptionsDialog;
import docking.widgets.EventTrigger;
import docking.widgets.OptionDialog;
import generic.util.WindowUtilities;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.OptionsService;
import ghidra.graph.AttributeFilters;
import ghidra.graph.job.GraphJobRunner;
import ghidra.graph.viewer.popup.*;
@ -107,29 +98,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
private static final Dimension PREFERRED_VIEW_SIZE = new Dimension(1000, 1000);
private static final Dimension PREFERRED_LAYOUT_SIZE = new Dimension(3000, 3000);
private Logger log = Logger.getLogger(DefaultGraphDisplay.class.getName());
// layout algorithm categories
static final String MIN_CROSS = "Hierarchical MinCross";
static final String VERT_MIN_CROSS = "Vertical Hierarchical MinCross";
private Map<String, String> displayProperties;
private Set<DockingActionIf> addedActions = new LinkedHashSet<>();
private GraphDisplayListener listener = new DummyGraphDisplayListener();
private String title;
private AttributedGraph graph;
private static String DEFAULT_EDGE_TYPE_PRIORITY_LIST =
"Fall-Through,"+
"Conditional-Return,"+
"Unconditional-Jump,"+
"Conditional-Jump,"+
"Unconditional-Call,"+
"Conditional-Call,"+
"Terminator,"+
"Computed,"+
"Indirection,"+
"Entry";
private static String DEFAULT_FAVORED_EDGES = "Fall-Through";
/**
* a unique id for this {@link GraphDisplay}
*/
@ -143,7 +121,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
/**
* The {@link PluginTool}
*/
private final PluginTool pluginTool;
private final PluginTool tool;
private final DefaultGraphDisplayComponentProvider componentProvider;
@ -182,13 +160,13 @@ public class DefaultGraphDisplay implements GraphDisplay {
private AttributeFilters edgeFilters;
private AttributeFilters vertexFilters;
private GhidraIconCache iconCache;
private GraphRenderer graphRenderer = new DefaultGraphRenderer();
/**
* Multi-selection is done in a free-form traced shape instead of a rectangle
*/
private boolean freeFormSelection;
/**
* Handles all mouse interaction
*/
@ -209,31 +187,32 @@ public class DefaultGraphDisplay implements GraphDisplay {
private SelectedEdgePaintable<AttributedVertex, AttributedEdge> selectedEdgePaintable;
private GraphDisplayOptions graphDisplayOptions = GraphDisplayOptions.DEFAULT;
private ChangeListener graphDisplayOptionsChangeListener;
private MultiStateDockingAction<String> layoutAction;
/**
* Create the initial display, the graph-less visualization viewer, and its controls
* @param displayProvider provides a {@link PluginTool} for Docking features
* @param displayProperties graph properties that will override the default graph properties
* @param id the unique display id
*/
DefaultGraphDisplay(DefaultGraphDisplayProvider displayProvider,
Map<String, String> displayProperties, int id) {
DefaultGraphDisplay(DefaultGraphDisplayProvider displayProvider, int id) {
this.graphDisplayProvider = displayProvider;
this.displayId = id;
this.pluginTool = graphDisplayProvider.getPluginTool();
this.displayProperties = displayProperties;
this.tool = graphDisplayProvider.getPluginTool();
this.viewer = createViewer();
buildHighlighers();
componentProvider = new DefaultGraphDisplayComponentProvider(this, pluginTool);
componentProvider = new DefaultGraphDisplayComponentProvider(this, tool);
componentProvider.addToTool();
satelliteViewer = createSatelliteViewer(viewer);
if (graphDisplayProvider.getDefaultSatelliteState()) {
viewer.getComponent().add(satelliteViewer.getComponent());
}
layoutTransitionManager =
new LayoutTransitionManager(viewer, this::isRoot,
getEdgeTypePriorityList(),
getFavoredEdgePredicate());
new LayoutTransitionManager(viewer, this::isRoot, graphRenderer);
viewer.getComponent().addComponentListener(new ComponentAdapter() {
@Override
@ -254,29 +233,26 @@ public class DefaultGraphDisplay implements GraphDisplay {
createToolbarActions();
createPopupActions();
connectSelectionStateListeners();
graphDisplayOptionsChangeListener = e -> refreshViewer();
}
private void refreshViewer() {
graphRenderer.clearCache();
graphRenderer.initializeViewer(viewer);
// bug in jungraphT library where vertex selection color doesn't update, but edge selection
// color does, so just rebuild the highlighter
buildHighlighers();
viewer.repaint();
}
private Color getSelectedVertexColor() {
String property = displayProperties.getOrDefault(SELECTED_VERTEX_COLOR, "0xFF0000");
return Colors.getHexColor(property);
return graphRenderer.getVertexSelectionColor();
}
private Color getSelectedEdgeColor() {
String property = displayProperties.getOrDefault(SELECTED_EDGE_COLOR, "0xFF0000");
return Colors.getHexColor(property);
}
private List<String> getEdgeTypePriorityList() {
return Arrays.asList(displayProperties
.getOrDefault(EDGE_TYPE_PRIORITY_LIST, DEFAULT_EDGE_TYPE_PRIORITY_LIST)
.split(","));
}
private Predicate<AttributedEdge> getFavoredEdgePredicate() {
String[] favoredEdges = displayProperties.getOrDefault(FAVORED_EDGES, DEFAULT_FAVORED_EDGES)
.split(",");
return attributedEdge -> Arrays.stream(favoredEdges)
.anyMatch(s -> s.equals(attributedEdge.getAttribute("EdgeType")));
return graphRenderer.getEdgeSelectionColor();
}
JComponent getComponent() {
@ -429,7 +405,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
// create a menu with graph layout algorithm selections
List<ActionState<String>> layoutActionStates = getLayoutActionStates();
new MultiStateActionBuilder<String>("Arrangement", ACTION_OWNER)
layoutAction = new MultiStateActionBuilder<String>("Arrangement", ACTION_OWNER)
.description("Arrangement: " + layoutActionStates.get(0).getName())
.toolBarIcon(DefaultDisplayGraphIcons.LAYOUT_ALGORITHM_ICON)
.useCheckboxForIcons(true)
@ -574,6 +550,14 @@ public class DefaultGraphDisplay implements GraphDisplay {
.onAction(c -> ungroupSelectedVertices())
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Graph Type Display Options", ACTION_OWNER)
.popupMenuPath("Graph Type Options ...")
.popupMenuGroup("zzz")
.menuPath("Graph Type Options ...")
.description("Brings up option editor for configuring vertex and edge types.")
.onAction(c -> editGraphDisplayOptions())
.buildAndInstallLocal(componentProvider);
togglePopupsAction = new ToggleActionBuilder("Display Popup Windows", ACTION_OWNER)
.popupMenuPath("Display Popup Windows")
.popupMenuGroup("zz", "1")
@ -585,6 +569,32 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private void editGraphDisplayOptions() {
String rootOptionsName = graphDisplayOptions.getRootOptionsName();
String relativePath = rootOptionsName + ".Vertex Colors";
// if the options are registered in the tool options, just show them
// otherwise, create a transient options and create an options dialog. This will
// allow the user to edit the options for the current graph instance.
if (graphDisplayOptions.isRegisteredWithTool()) {
OptionsService service = tool.getService(OptionsService.class);
service.showOptionsDialog("Graph." + relativePath, "");
}
else {
ToolOptions transientOptions = new ToolOptions("Graph");
HelpLocation help = new HelpLocation("GraphServices", "Graph Type Display Options");
graphDisplayOptions.registerOptions(transientOptions, help);
transientOptions.addOptionsChangeListener(graphDisplayOptions);
Options[] optionsArray = new Options[] { transientOptions };
String dialogTitle = "Graph Instance Settings (Not Saved in Tool Options)";
OptionsDialog dialog = new OptionsDialog(dialogTitle, "Graph", optionsArray, null);
// we have one less level for these transient tool options, so no need to prepend "graph."
dialog.displayCategory(relativePath, "");
tool.showDialog(dialog, componentProvider);
}
}
/**
* Group the selected vertices into one vertex that represents them all
*/
@ -638,7 +648,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
private void createAndDisplaySubGraph() {
GraphDisplay display = graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
try {
display.setGraph(createSubGraph(), title + " - Sub-graph", false, TaskMonitor.DUMMY);
display.setGraph(createSubGraph(), graphRenderer.getGraphDisplayOptions(),
title + " - Sub-graph", false, TaskMonitor.DUMMY);
display.setGraphDisplayListener(listener.cloneWith(display));
copyActionsToNewGraph((DefaultGraphDisplay) display);
}
@ -651,7 +662,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
Set<AttributedVertex> selected = viewer.getSelectedVertices();
Graph<AttributedVertex, AttributedEdge> subGraph = new AsSubgraph<>(graph, selected);
AttributedGraph newGraph = new AttributedGraph();
AttributedGraph newGraph =
new AttributedGraph(graph.getName() + ": subgraph", graph.getGraphType());
subGraph.vertexSet().forEach(newGraph::addVertex);
for (AttributedEdge e : subGraph.edgeSet()) {
AttributedVertex source = subGraph.getEdgeSource(e);
@ -667,17 +679,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
// select all the edges that connect the supplied vertices
private void selectEdgesConnecting(Collection<AttributedVertex> vertices) {
viewer.getSelectedEdgeState().select(
graph.edgeSet()
.stream()
.filter(
e -> {
AttributedVertex source = graph.getEdgeSource(e);
AttributedVertex target = graph.getEdgeTarget(e);
return vertices.contains(source)
&& vertices.contains(target);
})
.collect(Collectors.toSet()));
viewer.getSelectedEdgeState()
.select(
graph.edgeSet()
.stream()
.filter(
e -> {
AttributedVertex source = graph.getEdgeSource(e);
AttributedVertex target = graph.getEdgeTarget(e);
return vertices.contains(source) && vertices.contains(target);
})
.collect(Collectors.toSet()));
}
@ -782,7 +794,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private List<ActionState<String>> getLayoutActionStates() {
String[] names = layoutTransitionManager.getLayoutNames();
List<String> names = LayoutAlgorithmNames.getLayoutAlgorithmNames();
List<ActionState<String>> actionStates = new ArrayList<>();
for (String layoutName : names) {
ActionState<String> state = new ActionState<>(layoutName,
@ -790,11 +802,11 @@ public class DefaultGraphDisplay implements GraphDisplay {
// condense hierarchical action help to the top-level help description
String anchor = layoutName;
if (layoutName.contains(LayoutFunction.VERT_MIN_CROSS)) {
anchor = LayoutFunction.VERT_MIN_CROSS;
if (layoutName.contains(VERT_MIN_CROSS)) {
anchor = VERT_MIN_CROSS;
}
else if (layoutName.contains(LayoutFunction.MIN_CROSS)) {
anchor = LayoutFunction.MIN_CROSS;
else if (layoutName.contains(MIN_CROSS)) {
anchor = MIN_CROSS;
}
state.setHelpLocation(new HelpLocation(ACTION_OWNER, anchor));
@ -846,25 +858,21 @@ public class DefaultGraphDisplay implements GraphDisplay {
//
satellite.setGraphMouse(new JgtSatelliteGraphMouse());
satellite.getRenderContext().setEdgeDrawPaintFunction(Colors::getColor);
satellite.getRenderContext()
.setEdgeStrokeFunction(ProgramGraphFunctions::getEdgeStroke);
satellite.getRenderContext()
.setEdgeDrawPaintFunction(viewer.getRenderContext().getEdgeDrawPaintFunction());
satellite.getRenderContext()
.setVertexFillPaintFunction(viewer.getRenderContext().getVertexFillPaintFunction());
satellite.getRenderContext()
.setVertexDrawPaintFunction(viewer.getRenderContext().getVertexDrawPaintFunction());
RenderContext<AttributedVertex, AttributedEdge> renderer = satellite.getRenderContext();
RenderContext<AttributedVertex, AttributedEdge> viewerRenderer = viewer.getRenderContext();
renderer.setEdgeDrawPaintFunction(viewerRenderer.getEdgeDrawPaintFunction());
renderer.setEdgeStrokeFunction(viewerRenderer.getEdgeArrowStrokeFunction());
renderer.setEdgeDrawPaintFunction(viewerRenderer.getEdgeDrawPaintFunction());
renderer.setVertexFillPaintFunction(viewerRenderer.getVertexFillPaintFunction());
renderer.setVertexDrawPaintFunction(viewerRenderer.getVertexDrawPaintFunction());
satellite.scaleToLayout();
satellite.getRenderContext().setVertexLabelFunction(n -> null);
// always get the current predicate from the main view and test with it,
satellite.getRenderContext()
.setVertexIncludePredicate(
v -> viewer.getRenderContext().getVertexIncludePredicate().test(v));
satellite.getRenderContext()
.setEdgeIncludePredicate(
e -> viewer.getRenderContext().getEdgeIncludePredicate().test(e));
renderer.setVertexLabelFunction(n -> null);
// the satellite should use the same vertex predicate so that it has the same vertices
// as the main graph
renderer.setVertexIncludePredicate(v -> viewerRenderer.getVertexIncludePredicate().test(v));
renderer.setEdgeIncludePredicate(e -> viewerRenderer.getEdgeIncludePredicate().test(e));
satellite.getComponent().setBorder(BorderFactory.createEtchedBorder());
parentViewer.getComponent().addComponentListener(new ComponentAdapter() {
@Override
@ -885,6 +893,9 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
listener = null;
componentProvider.closeComponent();
if (graphDisplayOptions != null) {
graphDisplayOptions.removeChangeListener(graphDisplayOptionsChangeListener);
}
}
@Override
@ -1035,8 +1046,9 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private void setInitialLayoutAlgorithm() {
if (displayProperties.containsKey(INITIAL_LAYOUT_ALGORITHM)) {
String layoutAlgorithmName = displayProperties.get(INITIAL_LAYOUT_ALGORITHM);
String layoutAlgorithmName = graphDisplayOptions.getDefaultLayoutAlgorithmNameLayout();
layoutAction.setCurrentActionStateByUserData(layoutAlgorithmName);
if (layoutAlgorithmName != null) {
layoutTransitionManager.setLayout(layoutAlgorithmName);
}
else {
@ -1056,7 +1068,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
private boolean isRoot(AttributedVertex vertex) {
Set<AttributedEdge> incomingEdgesOf = graph.incomingEdgesOf(vertex);
return incomingEdgesOf.isEmpty() ||
graph.incomingEdgesOf(vertex).equals(graph.outgoingEdgesOf(vertex));
graph.incomingEdgesOf(vertex).equals(graph.outgoingEdgesOf(vertex));
}
/**
@ -1075,7 +1087,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
.elements(vertices)
.maxFactor(.05)
.buttonSupplier(JRadioButton::new)
.paintFunction(v -> Colors.VERTEX_TYPE_TO_COLOR_MAP.getOrDefault(v, Color.blue))
.paintFunction(v -> graphDisplayOptions.getVertexColor(v))
.build();
vertexFilters.addItemListener(item -> {
@ -1083,7 +1095,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
Set<String> selected = (Set<String>) item.getItem();
viewer.getRenderContext()
.setVertexIncludePredicate(
v -> v.getAttributeMap().values().stream().noneMatch(selected::contains));
v -> v.getAttributes().values().stream().noneMatch(selected::contains));
viewer.repaint();
});
@ -1093,7 +1105,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
.elements(edges)
.maxFactor(.01)
.buttonSupplier(JRadioButton::new)
.paintFunction(e -> Colors.EDGE_TYPE_TO_COLOR_MAP.getOrDefault(e, Color.green))
.paintFunction(e -> graphDisplayOptions.getEdgeColor(e))
.build();
edgeFilters.addItemListener(item -> {
@ -1101,7 +1113,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
Set<String> selected = (Set<String>) item.getItem();
viewer.getRenderContext()
.setEdgeIncludePredicate(
e -> e.getAttributeMap().values().stream().noneMatch(selected::contains));
e -> e.getAttributes().values().stream().noneMatch(selected::contains));
viewer.repaint();
});
}
@ -1124,36 +1136,21 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
}
@Override
public void defineVertexAttribute(String attributeName) {
log.fine("defineVertexAttribute " + attributeName + " is not implemented");
private void setGraphDisplayOptions(GraphDisplayOptions options) {
if (graphDisplayOptions != null) {
graphDisplayOptions.removeChangeListener(graphDisplayOptionsChangeListener);
}
graphDisplayOptions = options;
graphDisplayOptions.addChangeListener(graphDisplayOptionsChangeListener);
graphRenderer.setGraphTypeDisplayOptions(options);
refreshViewer();
}
@Override
public void defineEdgeAttribute(String attributeName) {
log.fine("defineEdgeAttribute " + attributeName + " is not implemented");
}
@Override
public void setVertexLabelAttribute(String attributeName, int alignment, int size,
boolean monospace,
int maxLines) {
log.fine("setVertexLabel " + attributeName);
this.iconCache.setPreferredVertexLabelAttribute(attributeName);
}
/**
* consume a {@link Graph} and display it
* @param graph the graph to display or consume
* @param title a title for 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
public void setGraph(AttributedGraph graph, String title, boolean append,
TaskMonitor monitor) {
iconCache.clear();
public void setGraph(AttributedGraph graph, GraphDisplayOptions options, String title,
boolean append, TaskMonitor monitor) {
setGraphDisplayOptions(options);
if (append && Objects.equals(title, this.title) && this.graph != null) {
graph = mergeGraphs(graph, this.graph);
}
@ -1165,12 +1162,12 @@ public class DefaultGraphDisplay implements GraphDisplay {
Msg.showWarn(this, null, "Graph Not Rendered - Too many nodes!",
"Exceeded limit of " + MAX_NODES + " nodes.\n\n Graph contained " + count +
" nodes!");
graph = new AttributedGraph();
graph = new AttributedGraph("Aborted", graph.getGraphType(), "Too Many Nodes");
graph.addVertex("1", "Graph Aborted");
}
doSetGraphData(graph);
graphCollapser = new GhidraGraphCollapser(viewer);
buildHighlighers();
}
private AttributedGraph mergeGraphs(AttributedGraph newGraph, AttributedGraph oldGraph) {
@ -1181,7 +1178,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
AttributedVertex from = oldGraph.getEdgeSource(edge);
AttributedVertex to = oldGraph.getEdgeTarget(edge);
AttributedEdge newEdge = newGraph.addEdge(from, to);
Map<String, String> attributeMap = edge.getAttributeMap();
Map<String, String> attributeMap = edge.getAttributes();
for (String key : attributeMap.keySet()) {
newEdge.setAttribute(key, edge.getAttribute(key));
}
@ -1265,7 +1262,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public void updateVertexName(AttributedVertex vertex, String newName) {
vertex.setName(newName);
iconCache.evict(vertex);
graphRenderer.vertexChanged(vertex);
viewer.repaint();
}
@ -1320,44 +1317,14 @@ public class DefaultGraphDisplay implements GraphDisplay {
PopupSource<AttributedVertex, AttributedEdge> popupSource = new GraphDisplayPopupSource(vv);
popupRegulator = new PopupRegulator<>(popupSource);
this.iconCache = new GhidraIconCache();
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
renderContext.getSelectedVertexState().addItemListener(item -> {
renderContext.getSelectedEdgeState().clear();
selectEdgesConnecting(renderContext.getSelectedVertexState().getSelected());
});
selectEdgesConnecting(renderContext.getSelectedVertexState().getSelected());
});
setVertexPreferences(vv);
renderContext.setEdgeStrokeFunction(e -> ProgramGraphFunctions.getEdgeStroke(e));
renderContext.setEdgeDrawPaintFunction(e -> Colors.getColor(e));
renderContext.setArrowDrawPaintFunction(e -> Colors.getColor(e));
renderContext.setArrowFillPaintFunction(e -> Colors.getColor(e));
// assign the shapes to the modal renderer
ModalRenderer<AttributedVertex, AttributedEdge> modalRenderer = vv.getRenderer();
// the modal renderer optimizes rendering for large graphs by removing detail
Renderer.Vertex<AttributedVertex, AttributedEdge> vertexRenderer =
modalRenderer.getVertexRenderer(LIGHTWEIGHT);
// cause the lightweight (optimized) renderer to use the vertex shapes instead
// of using default shapes.
if (vertexRenderer instanceof LightweightVertexRenderer) {
Function<AttributedVertex, Shape> vertexShapeFunction =
renderContext.getVertexShapeFunction();
LightweightVertexRenderer<AttributedVertex, AttributedEdge> lightweightVertexRenderer =
(LightweightVertexRenderer<AttributedVertex, AttributedEdge>) vertexRenderer;
lightweightVertexRenderer.setVertexShapeFunction(vertexShapeFunction);
}
renderContext.setVertexLabelRenderer(new JLabelVertexLabelRenderer(Color.black));
renderContext.setVertexDrawPaintFunction(Colors::getColor);
renderContext.setVertexFillPaintFunction(Colors::getColor);
renderContext.setVertexStrokeFunction(n -> new BasicStroke(3.0f));
renderContext.setEdgeShapeFunction(EdgeShape.line());
graphRenderer.initializeViewer(vv);
vv.getComponent().requestFocus();
vv.setBackground(Color.WHITE);
@ -1366,46 +1333,12 @@ public class DefaultGraphDisplay implements GraphDisplay {
vv.getComponent().removeMouseListener(mouseListener);
}
graphMouse = new JgtGraphMouse(this,
Boolean.parseBoolean(displayProperties.getOrDefault(ENABLE_EDGE_SELECTION,
"false")));
graphMouse = new JgtGraphMouse(this, false);
vv.setGraphMouse(graphMouse);
return vv;
}
private void setVertexPreferences(VisualizationViewer<AttributedVertex, AttributedEdge> vv) {
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
String useIcons =
displayProperties.getOrDefault(DISPLAY_VERTICES_AS_ICONS, Boolean.TRUE.toString());
Function<Shape, Rectangle> toRectangle = s -> RectangleUtils.convert(s.getBounds2D());
if (Boolean.parseBoolean(useIcons)) {
// set up the shape and color functions
IconShapeFunction<AttributedVertex> nodeShaper =
new IconShapeFunction<>(new EllipseShapeFunction<>());
nodeShaper.setIconFunction(iconCache::get);
renderContext.setVertexShapeFunction(nodeShaper);
renderContext.setVertexIconFunction(iconCache::get);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(nodeShaper.andThen(toRectangle))
.build());
}
else {
vv.getRenderContext().setVertexShapeFunction(ProgramGraphFunctions::getVertexShape);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(renderContext.getVertexShapeFunction()
.andThen(toRectangle))
.build());
vv.getRenderContext().setVertexLabelFunction(Object::toString);
vv.getRenderContext()
.setVertexLabelPosition(
VertexLabel.Position.valueOf(
displayProperties.getOrDefault(VERTEX_LABEL_POSITION, "AUTO")));
}
}
private void copyActionsToNewGraph(DefaultGraphDisplay display) {
for (DockingActionIf action : addedActions) {
@ -1454,7 +1387,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
return viewer.getSelectedVertices();
}
public ActionContext getActionContext(MouseEvent e) {
ActionContext getActionContext(MouseEvent e) {
AttributedVertex pickedVertex = JgtUtils.getVertex(e, viewer);
if (pickedVertex != null) {
@ -1596,39 +1529,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
}
private class AttributedToolTipInfo extends ToolTipInfo<Attributed> {
AttributedToolTipInfo(Attributed graphObject, MouseEvent event) {
super(event, graphObject);
}
@Override
public JComponent createToolTipComponent() {
if (graphObject == null) {
return null;
}
String toolTip = graphObject.getHtmlString();
if (StringUtils.isBlank(toolTip)) {
return null;
}
JToolTip jToolTip = new JToolTip();
jToolTip.setTipText(toolTip);
return jToolTip;
}
@Override
protected void emphasize() {
// this graph display does not have a notion of emphasizing
}
@Override
protected void deEmphasize() {
// this graph display does not have a notion of emphasizing
}
}
/**
* Item listener for selection changes in the graph with the additional
* capability of being able to disable the listener without removing it.

View file

@ -15,9 +15,7 @@
*/
package ghidra.graph.visualization;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import ghidra.framework.options.Options;
@ -54,13 +52,7 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
}
@Override
public GraphDisplay getGraphDisplay(boolean reuseGraph, TaskMonitor monitor) {
return getGraphDisplay(reuseGraph, Collections.emptyMap(), monitor);
}
@Override
public GraphDisplay getGraphDisplay(boolean reuseGraph, Map<String, String> properties,
TaskMonitor monitor) {
public GraphDisplay getGraphDisplay(boolean reuseGraph, TaskMonitor monitor) {
if (reuseGraph && !displays.isEmpty()) {
DefaultGraphDisplay visibleGraph = getVisibleGraph();
@ -69,7 +61,7 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
}
DefaultGraphDisplay display =
Swing.runNow(() -> new DefaultGraphDisplay(this, properties, displayCounter++));
Swing.runNow(() -> new DefaultGraphDisplay(this, displayCounter++));
displays.add(display);
return display;
}

View file

@ -0,0 +1,326 @@
/* ###
* 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 static org.jungrapht.visualization.renderers.BiModalRenderer.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.*;
import javax.swing.border.Border;
import org.jungrapht.visualization.RenderContext;
import org.jungrapht.visualization.VisualizationViewer;
import org.jungrapht.visualization.decorators.*;
import org.jungrapht.visualization.layout.algorithms.util.InitialDimensionFunction;
import org.jungrapht.visualization.renderers.*;
import org.jungrapht.visualization.renderers.Renderer;
import org.jungrapht.visualization.renderers.Renderer.VertexLabel.Position;
import org.jungrapht.visualization.util.RectangleUtils;
import generic.util.image.ImageUtils;
import ghidra.service.graph.*;
/**
* Handles the rendering of graphs for the {@link DefaultGraphDisplay}
*/
public class DefaultGraphRenderer implements GraphRenderer {
private static final int DEFAULT_MARGIN_BORDER_SIZE = 4;
private static final int DEFAULT_STROKE_THICKNESS = 6;
private static final int ICON_ZOOM = 5;
private int labelBorderSize = DEFAULT_MARGIN_BORDER_SIZE;
private int strokeThickness = DEFAULT_STROKE_THICKNESS;
private JLabel label;
private GraphDisplayOptions options;
private final Map<AttributedVertex, Icon> iconCache = new ConcurrentHashMap<>();
private final Map<RenderingHints.Key, Object> renderingHints = new HashMap<>();
private Stroke edgeStroke = new BasicStroke(4.0f);
public DefaultGraphRenderer() {
this(new DefaultGraphDisplayOptions());
}
public DefaultGraphRenderer(GraphDisplayOptions options) {
this.options = options;
renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
label = new JLabel();
label.setForeground(Color.black);
label.setBackground(Color.white);
label.setOpaque(false);
Border marginBorder = BorderFactory.createEmptyBorder(labelBorderSize, 2 * labelBorderSize,
labelBorderSize, 2 * labelBorderSize);
label.setBorder(marginBorder);
}
@Override
public void setGraphTypeDisplayOptions(GraphDisplayOptions options) {
this.options = options;
clearCache();
}
@Override
public GraphDisplayOptions getGraphDisplayOptions() {
return options;
}
@Override
public void clearCache() {
iconCache.clear();
}
@Override
public void initializeViewer(VisualizationViewer<AttributedVertex, AttributedEdge> viewer) {
RenderContext<AttributedVertex, AttributedEdge> renderContext = viewer.getRenderContext();
Function<Shape, org.jungrapht.visualization.layout.model.Rectangle> toRectangle =
s -> RectangleUtils.convert(s.getBounds2D());
if (options.usesIcons()) {
// set up the shape and color functions
IconShapeFunction<AttributedVertex> nodeShaper =
new IconShapeFunction<>(new EllipseShapeFunction<>());
nodeShaper.setIconFunction(this::getIcon);
renderContext.setVertexShapeFunction(nodeShaper);
renderContext.setVertexIconFunction(this::getIcon);
int arrowLength = options.getArrowLength() * ICON_ZOOM;
int arrowWidth = (int) (arrowLength * 1.3);
renderContext.setEdgeArrowWidth(arrowWidth);
renderContext.setEdgeArrowLength(arrowLength);
renderContext.setVertexLabelFunction(v -> "");
viewer.setInitialDimensionFunction(
InitialDimensionFunction.builder(nodeShaper.andThen(toRectangle)).build());
}
else {
renderContext.setVertexIconFunction(null);
renderContext.setVertexShapeFunction(this::getVertexShape);
viewer.setInitialDimensionFunction(InitialDimensionFunction
.builder(renderContext.getVertexShapeFunction().andThen(toRectangle))
.build());
renderContext.setVertexLabelFunction(Object::toString);
GraphLabelPosition labelPosition = options.getLabelPosition();
renderContext.setVertexLabelPosition(getJungraphTPosition(labelPosition));
}
// assign the shapes to the modal renderer
// the modal renderer optimizes rendering for large graphs by removing detail
ModalRenderer<AttributedVertex, AttributedEdge> modalRenderer = viewer.getRenderer();
Renderer.Vertex<AttributedVertex, AttributedEdge> lightWeightRenderer =
modalRenderer.getVertexRenderer(LIGHTWEIGHT);
// set the lightweight (optimized) renderer to use the vertex shapes instead
// of using default shapes.
if (lightWeightRenderer instanceof LightweightVertexRenderer) {
LightweightVertexRenderer<AttributedVertex, AttributedEdge> lightweightVertexRenderer =
(LightweightVertexRenderer<AttributedVertex, AttributedEdge>) lightWeightRenderer;
Function<AttributedVertex, Shape> vertexShapeFunction =
renderContext.getVertexShapeFunction();
lightweightVertexRenderer.setVertexShapeFunction(vertexShapeFunction);
}
renderContext.setVertexFontFunction(this::getFont);
renderContext.setVertexLabelRenderer(new JLabelVertexLabelRenderer(Color.black));
renderContext.setVertexDrawPaintFunction(this::getVertexColor);
renderContext.setVertexFillPaintFunction(this::getVertexColor);
renderContext.setVertexStrokeFunction(n -> new BasicStroke(3.0f));
renderContext.setEdgeStrokeFunction(this::getEdgeStroke);
renderContext.setEdgeDrawPaintFunction(this::getEdgeColor);
renderContext.setArrowDrawPaintFunction(this::getEdgeColor);
renderContext.setArrowFillPaintFunction(this::getEdgeColor);
renderContext.setEdgeShapeFunction(EdgeShape.line());
}
private Shape getVertexShape(AttributedVertex vertex) {
if (vertex instanceof GroupVertex) {
return VertexShape.STAR.getShape();
}
VertexShape vertexShape = options.getVertexShape(vertex);
return vertexShape != null ? vertexShape.getShape() : VertexShape.RECTANGLE.getShape();
}
private Position getJungraphTPosition(GraphLabelPosition labelPosition) {
switch (labelPosition) {
case CENTER:
return Position.CNTR;
case EAST:
return Position.E;
case NORTH:
return Position.N;
case NORTHEAST:
return Position.NE;
case NORTHWEST:
return Position.NW;
case SOUTH:
return Position.S;
case SOUTHEAST:
return Position.SE;
case SOUTHWEST:
return Position.SW;
case WEST:
return Position.W;
default:
return Position.AUTO;
}
}
private Color getVertexColor(AttributedVertex vertex) {
return options.getVertexColor(vertex);
}
private Color getEdgeColor(AttributedEdge edge) {
return options.getEdgeColor(edge);
}
private Icon getIcon(AttributedVertex vertex) {
// WARNING: very important to not use map's computeIfAbsent() method
// because the map is synchronized and the createIcon() method will
// attempt to acquire the AWT lock. That combination will cause a deadlock
// if computeIfAbsent() is used and this method is called from non-swing thread.
Icon icon = iconCache.get(vertex);
if (icon == null) {
icon = createIcon(vertex);
iconCache.put(vertex, icon);
}
return icon;
}
private Icon createIcon(AttributedVertex vertex) {
VertexShape vertexShape = options.getVertexShape(vertex);
Color vertexColor = options.getVertexColor(vertex);
String labelText = options.getVertexLabel(vertex);
return createImage(vertexShape, labelText, vertexColor);
}
@Override
public void vertexChanged(AttributedVertex vertex) {
iconCache.remove(vertex);
}
private ImageIcon createImage(VertexShape vertexShape, String vertexName, Color vertexColor) {
prepareLabel(vertexName, vertexColor);
Shape unitShape = vertexShape.getShape();
Rectangle bounds = unitShape.getBounds();
int maxWidthToHeightRatio = vertexShape.getMaxWidthToHeightRatio();
double sizeFactor = vertexShape.getShapeToLabelRatio();
int labelWidth = label.getWidth();
int labelHeight = label.getHeight();
int iconWidth =
(int) (Math.max(labelWidth, labelHeight * 2.0) * sizeFactor) + strokeThickness;
int iconHeight =
(int) (Math.max(label.getHeight(), labelWidth / maxWidthToHeightRatio) * sizeFactor) +
strokeThickness;
double scalex = iconWidth / bounds.getWidth();
double scaley = iconHeight / bounds.getHeight();
Shape scaledShape =
AffineTransform.getScaleInstance(scalex, scaley).createTransformedShape(unitShape);
double labelOffsetRatio = vertexShape.getLabelPosition();
bounds = scaledShape.getBounds();
int width = bounds.width + 2 * strokeThickness;
int height = bounds.height + strokeThickness;
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = bufferedImage.createGraphics();
graphics.setRenderingHints(renderingHints);
AffineTransform graphicsTransform = graphics.getTransform();
graphics.translate(-bounds.x + strokeThickness, -bounds.y + strokeThickness / 2);
graphics.setPaint(Color.WHITE);
graphics.fill(scaledShape);
graphics.setPaint(vertexColor);
graphics.setStroke(new BasicStroke(strokeThickness));
graphics.draw(scaledShape);
graphics.setTransform(graphicsTransform);
int xOffset = (width - label.getWidth()) / 2;
int yOffset = (int) ((height - label.getHeight()) * labelOffsetRatio);
graphics.translate(xOffset, yOffset);
graphics.setPaint(Color.black);
label.paint(graphics);
graphics.setTransform(graphicsTransform); // restore the original transform
graphics.dispose();
Image scaledImage =
ImageUtils.createScaledImage(bufferedImage, width * ICON_ZOOM, height * ICON_ZOOM,
Image.SCALE_FAST);
ImageIcon imageIcon = new ImageIcon(scaledImage);
return imageIcon;
}
private void prepareLabel(String vertexName, Color vertexColor) {
label.setFont(options.getFont());
label.setText(vertexName);
Dimension labelSize = label.getPreferredSize();
label.setSize(labelSize);
}
@Override
public String getFavoredEdgeType() {
return options.getFavoredEdgeType();
}
@Override
public Integer getEdgePriority(String edgeType) {
return options.getEdgePriority(edgeType);
}
private Stroke getEdgeStroke(AttributedEdge edge) {
return edgeStroke;
}
@Override
public Color getVertexSelectionColor() {
return options.getVertexSelectionColor();
}
@Override
public Color getEdgeSelectionColor() {
return options.getEdgeSelectionColor();
}
private Font getFont(AttributedVertex attributedvertex1) {
return options.getFont();
}
}

View file

@ -15,44 +15,42 @@
*/
package ghidra.graph.visualization;
import ghidra.service.graph.AttributedEdge;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.GraphType;
/**
* {@code Comparator} to order {@code AttributedEdge}s based on their position in a
* supplied {@code List}.
*
* Edge comparator that compares edges based on their edge type. The default renderer will use
* the order in which the edge types were defined in the {@link GraphType}.
*/
public class EdgeComparator implements Comparator<AttributedEdge> {
/**
* {@code Map} of EdgeType attribute value to integer priority
*/
private Map<String, Integer> edgePriorityMap = new HashMap();
private GraphRenderer renderer;
/**
* Create an instance and place the list values into the {@code edgePriorityMap}
* with a one-up counter expressing their relative priority
* @param edgePriorityList
*/
public EdgeComparator(List<String> edgePriorityList) {
edgePriorityList.forEach(s -> edgePriorityMap.put(s, edgePriorityList.indexOf(s)));
public EdgeComparator(GraphRenderer renderer) {
this.renderer = renderer;
}
/**
* {@inheritdoc}
* Compares the {@code AttributedEdge}s using their priority in the supplied {@code edgePriorityMap}
*/
@Override
public int compare(AttributedEdge edgeOne, AttributedEdge edgeTwo) {
return priority(edgeOne).compareTo(priority(edgeTwo));
public int compare(AttributedEdge edge1, AttributedEdge edge2) {
String edgeType1 = edge1.getEdgeType();
String edgeType2 = edge2.getEdgeType();
if (edgeType1 == null && edgeType2 == null) {
return 0;
}
if (edgeType1 == null) {
return 1;
}
if (edgeType2 == null) {
return -1;
}
Integer priority1 = renderer.getEdgePriority(edgeType1);
Integer priority2 = renderer.getEdgePriority(edgeType2);
return priority1.compareTo(priority2);
}
private Integer priority(AttributedEdge e) {
return edgePriorityMap.getOrDefault(e.getAttribute("EdgeType"), 0);
}
}

View file

@ -1,210 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import ghidra.service.graph.AttributedVertex;
public class GhidraIconCache {
private static final int DEFAULT_STROKE_THICKNESS = 12;
private static final int DEFAULT_FONT_SIZE = 12;
private static final String DEFAULT_FONT_NAME = "Dialog";
private static final int DEFAULT_MARGIN_BORDER_SIZE = 8;
private static final float LABEL_TO_ICON_PROPORTION = 1.1f;
private final JLabel rendererLabel = new JLabel();
private final Map<RenderingHints.Key, Object> renderingHints = new HashMap<>();
private int strokeThickness = DEFAULT_STROKE_THICKNESS;
private final Map<AttributedVertex, Icon> map = new ConcurrentHashMap<>();
private final IconShape.Function iconShapeFunction = new IconShape.Function();
private String preferredVeretxLabelAttribute = null;
Icon get(AttributedVertex vertex) {
// WARNING: very important to not use map's computeIfAbsent() method
// because the map is synchronized and the createIcon() method will
// attempt to acquire the AWT lock. That combination will cause a deadlock
// if computeIfAbsent() is used and this method is called from non-swing thread.
Icon icon = map.get(vertex);
if (icon == null) {
icon = createIcon(vertex);
map.put(vertex, icon);
}
return icon;
}
GhidraIconCache() {
renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
private Icon createIcon(AttributedVertex vertex) {
rendererLabel
.setText(ProgramGraphFunctions.getLabel(vertex, preferredVeretxLabelAttribute));
rendererLabel.setFont(new Font(DEFAULT_FONT_NAME, Font.BOLD, DEFAULT_FONT_SIZE));
rendererLabel.setForeground(Color.black);
rendererLabel.setBackground(Color.white);
rendererLabel.setOpaque(true);
Border lineBorder = BorderFactory.createLineBorder((Color) Colors.getColor(vertex), 2);
Border marginBorder = BorderFactory.createEmptyBorder(DEFAULT_MARGIN_BORDER_SIZE,
DEFAULT_MARGIN_BORDER_SIZE, DEFAULT_MARGIN_BORDER_SIZE, DEFAULT_MARGIN_BORDER_SIZE);
rendererLabel.setBorder(new CompoundBorder(lineBorder, marginBorder));
Dimension labelSize = rendererLabel.getPreferredSize();
rendererLabel.setSize(labelSize);
Shape shape = ProgramGraphFunctions.getVertexShape(vertex);
IconShape.Type shapeType = iconShapeFunction.apply(shape);
return createImageIcon(vertex, shapeType, rendererLabel, labelSize, shape);
}
/**
* Based on the shape and characteristics of the vertex label (color, text) create and cache an ImageIcon
* that will be used to draw the vertex
*
* @param vertex the vertex to draw (and the key for the cache)
* @param vertexShapeCategory the type of Ghidra vertex shape
* @param label the {@link JLabel} used to draw the label. Note that it will parse html for formatting.
* @param labelSize the dimensions of the JLabel after it has been parsed
* @param vertexShape the primitive {@link Shape} used to represent the vertex
*/
private Icon createImageIcon(AttributedVertex vertex, IconShape.Type vertexShapeCategory,
JLabel label, Dimension labelSize, Shape vertexShape) {
int offset = 0;
double scalex;
double scaley;
switch (vertexShapeCategory) {
// triangles have a non-zero +/- yoffset instead of centering the label
case TRIANGLE:
// scale the vertex shape
scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() *
LABEL_TO_ICON_PROPORTION;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() *
LABEL_TO_ICON_PROPORTION;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape);
offset = -(int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2);
break;
case INVERTED_TRIANGLE:
scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth() *
LABEL_TO_ICON_PROPORTION;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() *
LABEL_TO_ICON_PROPORTION;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape);
offset = (int) ((vertexShape.getBounds().getHeight() - labelSize.getHeight()) / 2);
break;
// rectangles can fit a full-sized label
case RECTANGLE:
scalex = labelSize.getWidth() / vertexShape.getBounds().getWidth();
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight();
vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape);
break;
// diamonds and ellipses reduce the label size to fit
case DIAMOND:
default: // ELLIPSE
scalex =
labelSize.getWidth() / vertexShape.getBounds().getWidth() * 1.1;
scaley = labelSize.getHeight() / vertexShape.getBounds().getHeight() * 1.1;
vertexShape = AffineTransform.getScaleInstance(scalex, scaley)
.createTransformedShape(vertexShape);
break;
}
Rectangle vertexBounds = vertexShape.getBounds();
BufferedImage bufferedImage = new BufferedImage(vertexBounds.width + (2 * strokeThickness),
vertexBounds.height + (2 * strokeThickness), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = bufferedImage.createGraphics();
graphics.setRenderingHints(renderingHints);
AffineTransform graphicsTransform = graphics.getTransform();
// draw the shape, offset by 1/2 its width and the strokeThickness
AffineTransform offsetTransform =
AffineTransform.getTranslateInstance(strokeThickness + vertexBounds.width / 2.0,
strokeThickness + vertexBounds.height / 2.0);
offsetTransform.preConcatenate(graphicsTransform);
graphics.setTransform(offsetTransform);
graphics.setPaint(Color.white);
graphics.fill(vertexShape);
graphics.setPaint(Colors.getColor(vertex));
graphics.setStroke(new BasicStroke(strokeThickness));
graphics.draw(vertexShape);
// draw the JLabel, offset by 1/2 its width and the strokeThickness
int xoffset = strokeThickness + (vertexBounds.width - labelSize.width) / 2;
int yoffset = strokeThickness + (vertexBounds.height - labelSize.height) / 2;
offsetTransform = AffineTransform.getTranslateInstance(xoffset, yoffset + offset);
offsetTransform.preConcatenate(graphicsTransform);
graphics.setPaint(Color.black);
graphics.setTransform(offsetTransform);
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.dispose();
return new ImageIcon(bufferedImage);
}
public void 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);
}
/**
* Sets the vertex label to the value of the passed attribute name
* @param attributeName the attribute key for the vertex label value to be displayed
*/
public void setPreferredVertexLabelAttribute(String attributeName) {
this.preferredVeretxLabelAttribute = attributeName;
}
}

View file

@ -0,0 +1,90 @@
/* ###
* 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.Color;
import org.jungrapht.visualization.VisualizationViewer;
import ghidra.service.graph.*;
/**
* Interface for GraphRenderer used by the {@link DefaultGraphDisplay}. Developers can add new
* implementations to change the graph rendering
*/
public interface GraphRenderer {
/**
* Initializes the {@link VisualizationViewer}. When a new {@link DefaultGraphDisplay} is
* created, it uses a JungraphT {@link VisualizationViewer} to display a graph. That viewer
* has many configuration settings. The GraphRender needs to initialize the viewer so that
* it calls back to this renderer to get the vertex and edge data/functions that it needs
* to render a graph. This is how the GraphRender can inject is display style into the graph
* display.
* <P>
* @param viewer the {@link VisualizationViewer}
*/
public void initializeViewer(VisualizationViewer<AttributedVertex, AttributedEdge> viewer);
/**
* Sets the graph display options that are specific to a particular graph type
* @param options the {@link GraphDisplayOptions} which are options for a specific graph type
*/
public void setGraphTypeDisplayOptions(GraphDisplayOptions options);
/**
* Returns the current {@link GraphDisplayOptions} being used
* @return the current {@link GraphDisplayOptions} being used
*/
public GraphDisplayOptions getGraphDisplayOptions();
/**
* Tells this renderer that the given vertex changed and needs to be redrawn
* @param vertex the vertex that changed
*/
public void vertexChanged(AttributedVertex vertex);
/**
* Returns the favored edge type
* @return the favored edge type
*/
public String getFavoredEdgeType();
/**
* Returns the edge priority for the edge type
* @param edgeType the edge type to get priority for
* @return the edge priority for the edge type
*/
public Integer getEdgePriority(String edgeType);
/**
* Clears any cached renderings
*/
public void clearCache();
/**
* Returns the vertex selection color
* @return the vertex selection color
*/
public Color getVertexSelectionColor();
/**
* Returns the edge selection color
* @return the edge selection color
*/
public Color getEdgeSelectionColor();
}

View file

@ -46,8 +46,7 @@ public class GroupVertex extends AttributedVertex {
super(id);
this.first = first;
this.children = children;
setAttribute("VertexType", "Collapsed");
setAttribute("Icon", "Star");
setVertexType("Collapsed Group");
}
/**

View file

@ -1,104 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import java.awt.Shape;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
/**
* Holds the enum for shape type and the Function to categorize the archetype Shapes into
* IconShape.Types. Note that the archetype shapes are centered at the origin
*/
public class IconShape {
public enum Type {
TRIANGLE, INVERTED_TRIANGLE, RECTANGLE, DIAMOND, ELLIPSE
}
/**
* Categorize the supplied Shape into one of several simple types.
*
*/
static class Function implements java.util.function.Function<Shape, Type> {
@Override
public Type apply(Shape shape) {
List<Point2D> points = getShapePoints(shape);
if (points.size() == 3) {
if (isInvertedTriangle(points)) {
return Type.INVERTED_TRIANGLE;
} else {
return Type.TRIANGLE;
}
}
// there are 5 points because the final point is the same as the first
// and closes the shape.
if (points.size() == 5) {
if (isDiamond(points)) {
return Type.DIAMOND;
} else {
return Type.RECTANGLE;
}
}
// default to ellipse for anything with more that 4 sides
return Type.ELLIPSE;
}
/**
*
* Note that for awt drawing, the origin is at the upper left so positive y extends downwards.
* @param threePoints odd number of points bounding a {@link Shape} centered at the origin
* @return true it there are fewer points with y below 0
*/
boolean isInvertedTriangle(List<Point2D> threePoints) {
if (threePoints.size() != 3) {
throw new IllegalArgumentException("Shape from " + threePoints + " is not a triangle");
}
return threePoints.stream().filter(p -> p.getY() < 0).count() <= threePoints.size() / 2;
}
/**
*
* @param fivePoints odd number of points bounding a {@link Shape} centered at the origin
* @return true it there are 2 points with y value 0
*/
boolean isDiamond(List<Point2D> fivePoints) {
if (fivePoints.size() != 5) {
throw new IllegalArgumentException(
"Shape from " + fivePoints + " is not a quadrilateral");
}
return fivePoints.stream().filter(p -> (int) p.getY() == 0).count() == 2;
}
List<Point2D> getShapePoints(Shape shape) {
float[] seg = new float[6];
List<Point2D> points = new ArrayList<>();
for (PathIterator i = shape.getPathIterator(null, 1); !i.isDone(); i.next()) {
int ret = i.currentSegment(seg);
if (ret == PathIterator.SEG_MOVETO) {
points.add(new Point2D.Float(seg[0], seg[1]));
}
else if (ret == PathIterator.SEG_LINETO) {
points.add(new Point2D.Float(seg[0], seg[1]));
}
}
return points;
}
}
}

View file

@ -15,6 +15,8 @@
*/
package ghidra.graph.visualization;
import static ghidra.service.graph.LayoutAlgorithmNames.*;
import java.util.Comparator;
import java.util.function.Function;
import java.util.function.Predicate;
@ -23,6 +25,8 @@ import org.jungrapht.visualization.layout.algorithms.*;
import org.jungrapht.visualization.layout.algorithms.repulsion.BarnesHutFRRepulsion;
import org.jungrapht.visualization.layout.algorithms.sugiyama.Layering;
import com.google.common.base.Objects;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
@ -36,61 +40,32 @@ import ghidra.service.graph.AttributedVertex;
class LayoutFunction
implements Function<String, LayoutAlgorithm.Builder<AttributedVertex, ?, ?>> {
static final String KAMADA_KAWAI = "Force Balanced";
static final String FRUCTERMAN_REINGOLD = "Force Directed";
static final String CIRCLE = "Circle";
static final String TIDIER_TREE = "Compact Hierarchical";
static final String TIDIER_RADIAL_TREE = "Compact Radial";
static final String MIN_CROSS = "Hierarchical MinCross"; //not an alg, just a parent category
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_NETWORK_SIMPLEX = "Hierarchical MinCross Network Simplex";
static final String MIN_CROSS_COFFMAN_GRAHAM = "Hierarchical MinCross Coffman Graham";
static final String VERT_MIN_CROSS = "Vertical Hierarchical MinCross"; //not an alg, just a parent category
static final String VERT_MIN_CROSS_TOP_DOWN = "Vertical Hierarchical MinCross Top Down";
static final String VERT_MIN_CROSS_LONGEST_PATH = "Vertical Hierarchical MinCross Longest Path";
static final String VERT_MIN_CROSS_NETWORK_SIMPLEX = "Vertical Hierarchical MinCross Network Simplex";
static final String VERT_MIN_CROSS_COFFMAN_GRAHAM = "Vertical Hierarchical MinCross Coffman Graham";
static final String TREE = "Hierarchical";
static final String RADIAL = "Radial";
static final String BALLOON = "Balloon";
static final String GEM = "GEM";
Predicate<AttributedEdge> favoredEdgePredicate;
Comparator<AttributedEdge> edgeTypeComparator;
LayoutFunction(Comparator<AttributedEdge> edgeTypeComparator, Predicate<AttributedEdge> favoredEdgePredicate) {
this.edgeTypeComparator = edgeTypeComparator;
this.favoredEdgePredicate = favoredEdgePredicate;
LayoutFunction(GraphRenderer renderer) {
this.edgeTypeComparator = new EdgeComparator(renderer);
this.favoredEdgePredicate =
edge -> Objects.equal(edge.getEdgeType(), renderer.getFavoredEdgeType());
}
public String[] getNames() {
return new String[] { TIDIER_TREE, TREE,
TIDIER_RADIAL_TREE, MIN_CROSS_TOP_DOWN, MIN_CROSS_LONGEST_PATH,
MIN_CROSS_NETWORK_SIMPLEX, MIN_CROSS_COFFMAN_GRAHAM, CIRCLE,
VERT_MIN_CROSS_TOP_DOWN,
VERT_MIN_CROSS_LONGEST_PATH,
VERT_MIN_CROSS_NETWORK_SIMPLEX,
VERT_MIN_CROSS_COFFMAN_GRAHAM,
KAMADA_KAWAI, FRUCTERMAN_REINGOLD, RADIAL, BALLOON, GEM
};
}
@Override
public LayoutAlgorithm.Builder<AttributedVertex, ?, ?> apply(String name) {
switch(name) {
case GEM:
return GEMLayoutAlgorithm.edgeAwareBuilder();
case KAMADA_KAWAI:
case FORCED_BALANCED:
return KKLayoutAlgorithm.<AttributedVertex> builder()
.preRelaxDuration(1000);
case FRUCTERMAN_REINGOLD:
case FORCE_DIRECTED:
return FRLayoutAlgorithm.<AttributedVertex> builder()
.repulsionContractBuilder(BarnesHutFRRepulsion.builder());
case CIRCLE:
return CircleLayoutAlgorithm.<AttributedVertex> builder()
.reduceEdgeCrossing(false);
case TIDIER_RADIAL_TREE:
case COMPACT_RADIAL:
return TidierRadialTreeLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder()
.edgeComparator(edgeTypeComparator);
@ -146,10 +121,10 @@ class LayoutFunction
return BalloonLayoutAlgorithm
.<AttributedVertex> builder()
.verticalVertexSpacing(300);
case TREE:
case HIERACHICAL:
return EdgeAwareTreeLayoutAlgorithm
.<AttributedVertex, AttributedEdge>edgeAwareBuilder();
case TIDIER_TREE:
case COMPACT_HIERACHICAL:
default:
return TidierTreeLayoutAlgorithm
.<AttributedVertex, AttributedEdge> edgeAwareBuilder()

View file

@ -15,10 +15,6 @@
*/
package ghidra.graph.visualization;
import static ghidra.graph.visualization.LayoutFunction.*;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
@ -30,8 +26,7 @@ import org.jungrapht.visualization.layout.model.Rectangle;
import org.jungrapht.visualization.util.LayoutAlgorithmTransition;
import org.jungrapht.visualization.util.LayoutPaintable;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
import ghidra.service.graph.*;
/**
* Manages the selection and transition from one {@link LayoutAlgorithm} to another
@ -73,14 +68,13 @@ class LayoutTransitionManager {
public LayoutTransitionManager(
VisualizationServer<AttributedVertex, AttributedEdge> visualizationServer,
Predicate<AttributedVertex> rootPredicate,
List<String> edgeTypePriorityList,
Predicate<AttributedEdge> favoredEdgePredicate) {
GraphRenderer renderer) {
this.visualizationServer = visualizationServer;
this.rootPredicate = rootPredicate;
this.renderContext = visualizationServer.getRenderContext();
this.vertexBoundsFunction = visualizationServer.getRenderContext().getVertexBoundsFunction();
this.layoutFunction = new LayoutFunction(new EdgeComparator(edgeTypePriorityList),
favoredEdgePredicate);
this.layoutFunction = new LayoutFunction(renderer);
}
/**
@ -144,7 +138,7 @@ class LayoutTransitionManager {
*/
public LayoutAlgorithm<AttributedVertex> getInitialLayoutAlgorithm() {
LayoutAlgorithm<AttributedVertex> initialLayoutAlgorithm =
layoutFunction.apply(TIDIER_TREE).build();
layoutFunction.apply(LayoutAlgorithmNames.COMPACT_HIERACHICAL).build();
if (initialLayoutAlgorithm instanceof TreeLayout) {
((TreeLayout<AttributedVertex>) initialLayoutAlgorithm)
@ -157,11 +151,4 @@ class LayoutTransitionManager {
return initialLayoutAlgorithm;
}
/**
* Supplies a {@code String[]} array of the supported layout names
* @return
*/
public String[] getLayoutNames() {
return layoutFunction.getNames();
}
}

View file

@ -1,140 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import static org.jungrapht.visualization.layout.util.PropertyLoader.*;
import java.awt.*;
import java.util.Map;
import org.apache.commons.text.StringEscapeUtils;
import org.jungrapht.visualization.util.ShapeFactory;
import com.google.common.base.Splitter;
import ghidra.service.graph.Attributed;
import ghidra.service.graph.AttributedEdge;
/**
* a container for various functions used by ProgramGraph
*/
abstract class ProgramGraphFunctions {
static float edgeWidth = Float.parseFloat(System.getProperty(PREFIX + "edgeWidth", "4.0f"));
// cannot instantiate nor extend
private ProgramGraphFunctions() {
}
/**
* a default implementation of a {@link ShapeFactory} to supply shapes for attributed vertices and edges
*/
private static ShapeFactory<Attributed> shapeFactory = new ShapeFactory<>(n -> 50, n -> 1.0f);
/**
* return various 'Shapes' based on an attribute name
*
* @param n the attributed key (a vertex or edge)
* @param name the attribute name
* @return a Shape for the passed 'n' with attribute 'name'
*/
private static Shape byShapeName(Attributed n, String name) {
if (name == null) {
return null;
}
switch (name) {
case "Square":
return shapeFactory.getRectangle(n);
case "Circle":
return shapeFactory.getEllipse(n);
case "Triangle":
return shapeFactory.getRegularPolygon(n, 3);
case "TriangleDown":
return shapeFactory.getRegularPolygon(n, 3, Math.PI);
case "Diamond":
return shapeFactory.getRectangle(n, Math.PI / 4);
case "Star":
return shapeFactory.getRegularStar(n, 5);
case "Pentagon":
return shapeFactory.getRegularPolygon(n, 5);
case "Hexagon":
return shapeFactory.getRegularPolygon(n, 6);
case "Octagon":
return shapeFactory.getRegularPolygon(n, 8);
default:
return null;
}
}
/*
* Gets the Shape object to use when drawing this vertex. If "Icon" attribute
* is set it will use that, otherwise "VertexType" to will translate a code flow
* name to a shape
*
* @param vertex the Attributed object to get a shape for
* @return a Shape object to use when displaying the object
*/
public static Shape getVertexShape(Attributed vertex) {
Shape shape = byShapeName(vertex, vertex.getAttribute("Icon"));
if (shape != null) {
return shape;
}
String vertexType = vertex.getAttribute("VertexType");
if (vertexType == null) {
return shapeFactory.getRectangle(vertex);
}
switch (vertexType) {
case "Entry":
return shapeFactory.getRegularPolygon(vertex, 3, Math.PI);
case "Exit":
return shapeFactory.getRegularPolygon(vertex, 3);
case "Switch":
return shapeFactory.getRectangle(vertex, Math.PI / 4);
case "Body":
case "External":
return shapeFactory.getRectangle(vertex);
default:
return shapeFactory.getEllipse(vertex);
}
}
/**
* Provides a {@link Stroke} (line width and style) for an attributed edge
* @param edge the edge to get a stroke value
* @return the stroke for the edge
*/
public static Stroke getEdgeStroke(AttributedEdge edge) {
String edgeType = edge.getAttribute("EdgeType");
if (edgeType != null && edgeType.equals("Fall-Through")) {
return new BasicStroke(edgeWidth * 2);
}
return new BasicStroke(edgeWidth);
}
/**
* gets a display label from an {@link Attributed} object (vertex)
* @param attributed the attributed object to get a label for
* @param preferredLabelAttribute the attribute to use for the label, if available
* @return the label for the given {@link Attributed}
*/
public static String getLabel(Attributed attributed, String preferredLabelAttribute) {
Map<String, String> map = attributed.getAttributeMap();
String name = StringEscapeUtils.escapeHtml4(map.get("Name"));
if (map.containsKey(preferredLabelAttribute)) {
name = StringEscapeUtils.escapeHtml4(map.get(preferredLabelAttribute));
}
return "<html>" + String.join("<p>", Splitter.on('\n').split(name));
}
}

View file

@ -73,4 +73,8 @@ public class JgtGraphMouse extends DefaultGraphMouse<AttributedVertex, Attribute
setPluginsLoaded();
}
public boolean allowsEdgeSelection() {
return allowEdgeSelection;
}
}

View file

@ -32,7 +32,7 @@ jungrapht.edgeWidth=4.0f
# stroke size for the magnifier lens
jungrapht.lensStrokeWidth=10.0
# when scale is < .1, switch to lightweight rendering
jungrapht.lightweightScaleThreshold=.1
jungrapht.lightweightScaleThreshold=.01
# under 50 vertices will use heavyweight rendering all the time
jungrapht.lightweightCountThreshold=80

View file

@ -277,7 +277,7 @@ public class AttributedGraphExportersTest extends AbstractGenericTest {
}
private AttributedGraph createGraph() {
AttributedGraph g = new AttributedGraph();
AttributedGraph g = new AttributedGraph("Test", new EmptyGraphType());
AttributedVertex vA = g.addVertex("A");
AttributedVertex vB = g.addVertex("B");
AttributedVertex vC = g.addVertex("C");

View file

@ -311,7 +311,7 @@ public class GraphExportTest extends AbstractGhidraHeadedIntegrationTest {
}
private AttributedGraph createGraph() {
AttributedGraph graph = new AttributedGraph();
AttributedGraph graph = new AttributedGraph("Test", new EmptyGraphType());
AttributedVertex vA = graph.addVertex("A");
AttributedVertex vB = graph.addVertex("B");
AttributedVertex vC = graph.addVertex("C");

View file

@ -1,75 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import static org.junit.Assert.*;
import java.awt.Color;
import org.junit.Test;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
public class ColorsTest {
@Test
public void testParseHashHexColor() {
Color hexColor = Colors.getHexColor("#ff0000");
assertEquals(Color.RED, hexColor);
}
@Test
public void testParseHexColor() {
Color hexColor = Colors.getHexColor("0xff0000");
assertEquals(Color.RED, hexColor);
}
@Test
public void testGetColorFromVertexType() {
AttributedVertex vertex = new AttributedVertex("A");
vertex.setAttribute("VertexType", "Exit");
vertex.setAttribute("Color", "0xffffff");
assertEquals(Color.MAGENTA, Colors.getColor(vertex));
}
@Test
public void testGetColorFromVertexNoVertexType() {
AttributedVertex vertex = new AttributedVertex("A");
vertex.setAttribute("Color", "0xffffff");
assertEquals(Color.WHITE, Colors.getColor(vertex));
}
@Test
public void testGetColorFromVertexNoAttributes() {
AttributedVertex vertex = new AttributedVertex("A");
assertEquals(Color.GREEN, Colors.getColor(vertex));
}
@Test
public void testGetColorFromEdgeType() {
AttributedEdge edge = new AttributedEdge("A");
edge.setAttribute("EdgeType", "Computed");
edge.setAttribute("Color", "0xffffff");
assertEquals(Color.CYAN, Colors.getColor(edge));
}
@Test
public void testGetColorFromEdgeNoEdgeType() {
AttributedEdge edge = new AttributedEdge("A");
edge.setAttribute("Color", "0xffffff");
assertEquals(Color.WHITE, Colors.getColor(edge));
}
}

View file

@ -1,70 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.visualization;
import ghidra.service.graph.AttributedVertex;
import org.junit.Assert;
import org.junit.Test;
import java.awt.geom.Rectangle2D;
public class IconShapeTest {
private IconShape.Function iconShapeFunction = new IconShape.Function();
@Test
public void testShapes() {
Rectangle2D rectangle = new Rectangle2D.Double(-10, -10, 20, 20);
Assert.assertEquals(IconShape.Type.RECTANGLE, iconShapeFunction.apply(rectangle));
AttributedVertex v = new AttributedVertex("id", "name");
// by vertex type
v.setAttribute("VertexType", "Entry");
Assert.assertEquals(IconShape.Type.TRIANGLE,
iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("VertexType", "Exit");
Assert.assertEquals(IconShape.Type.INVERTED_TRIANGLE,
iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("VertexType", "Switch");
Assert.assertEquals(IconShape.Type.DIAMOND, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("VertexType", "Body");
Assert.assertEquals(IconShape.Type.RECTANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("VertexType", "External");
Assert.assertEquals(IconShape.Type.RECTANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("VertexType", "Foo");
Assert.assertEquals(IconShape.Type.ELLIPSE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
// by vertex icon shape name
v.removeAttribute("VertexType");
v.setAttribute("Icon", "Square");
Assert.assertEquals(IconShape.Type.RECTANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("Icon", "TriangleDown");
Assert.assertEquals(IconShape.Type.TRIANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("Icon", "Triangle");
Assert.assertEquals(IconShape.Type.INVERTED_TRIANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("Icon", "Diamond");
Assert.assertEquals(IconShape.Type.DIAMOND, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("Icon", "Circle");
Assert.assertEquals(IconShape.Type.ELLIPSE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
v.setAttribute("Icon", "Foo");
Assert.assertEquals(IconShape.Type.RECTANGLE, iconShapeFunction.apply(ProgramGraphFunctions.getVertexShape(v)));
}
}