mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 10:49:34 +02:00
GT-3019 - Function Graph - Edge Routing - review fixes:
-fixed broken edge labels (labels were not offset correctly) -fixed complex edge routing for edges going upward -fixed bug that allowed toolbar multi-state actions to fire twice when clicked
This commit is contained in:
parent
17b8e54f25
commit
d536c261f9
5 changed files with 235 additions and 116 deletions
|
@ -33,6 +33,7 @@ import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
|
|||
import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
|
||||
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
|
||||
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
|
||||
import ghidra.program.model.symbol.FlowType;
|
||||
|
||||
/**
|
||||
* An edge label renderer used with the {@link DecompilerNestedLayout}
|
||||
|
@ -86,11 +87,18 @@ class DNLEdgeLabelRenderer<V extends FGVertex, E extends FGEdge>
|
|||
double cx = start.getX();
|
||||
double cy = start.getY();
|
||||
|
||||
EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer();
|
||||
Font font = rc.getEdgeFontTransformer().apply(e);
|
||||
boolean isSelected = rc.getPickedEdgeState().isPicked(e);
|
||||
Component component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(),
|
||||
text, font, isSelected, e);
|
||||
int labelWidth = component.getPreferredSize().width;
|
||||
|
||||
List<Point2D> articulationPoints = e.getArticulationPoints();
|
||||
if (articulationPoints.isEmpty()) {
|
||||
double vertexBottom = start.getY() + (vertexBounds.height >> 1); // location is centered
|
||||
int textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend
|
||||
int textX = (int) (start.getX() + xDisplacement); // right of the edge
|
||||
double textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend
|
||||
double textX = (int) (start.getX() + xDisplacement); // right of the edge
|
||||
labelPointOffset.setLocation(textX, textY);
|
||||
}
|
||||
else if (articulationPoints.size() == 1) {
|
||||
|
@ -109,29 +117,26 @@ class DNLEdgeLabelRenderer<V extends FGVertex, E extends FGEdge>
|
|||
|
||||
double bx1 = bend1.getX();
|
||||
|
||||
FlowType flow = e.getFlowType();
|
||||
boolean isRight = flow.isFallthrough() || flow.isUnConditional();
|
||||
|
||||
if (articulationPoints.size() == 2) {
|
||||
|
||||
int textX = (int) (vertexSide + edgeOffset); // right of the vertex
|
||||
int textY = (int) (cy + edgeOffset); // above the edge
|
||||
double textX = (int) (vertexSide + edgeOffset); // right of the vertex
|
||||
double textY = (int) (cy + edgeOffset); // above the edge
|
||||
labelPointOffset.setLocation(textX, textY);
|
||||
}
|
||||
else if (articulationPoints.size() == 3) {
|
||||
else { // 3 or 4 articulations
|
||||
|
||||
double textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend
|
||||
double textX = (int) (bx1 + xDisplacement); // right of the edge
|
||||
if (!isRight) {
|
||||
textX = bx1 - xDisplacement - labelWidth;
|
||||
}
|
||||
|
||||
int textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend
|
||||
int textX = (int) (bx1 + xDisplacement); // right of the edge
|
||||
labelPointOffset.setLocation(textX, textY);
|
||||
}
|
||||
else { // if (articulationPoints.size() == 4) {
|
||||
int textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend
|
||||
int textX = (int) (bx1 + xDisplacement); // right of the edge
|
||||
labelPointOffset.setLocation(textX, textY);
|
||||
}
|
||||
}
|
||||
EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer();
|
||||
Font font = rc.getEdgeFontTransformer().apply(e);
|
||||
boolean isSelected = rc.getPickedEdgeState().isPicked(e);
|
||||
Component component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(),
|
||||
text, font, isSelected, e);
|
||||
|
||||
Dimension d = component.getPreferredSize();
|
||||
|
||||
|
@ -142,7 +147,37 @@ class DNLEdgeLabelRenderer<V extends FGVertex, E extends FGEdge>
|
|||
|
||||
g.setTransform(xform);
|
||||
g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true);
|
||||
|
||||
g.setTransform(old);
|
||||
|
||||
// debug
|
||||
//labelArticulations(component, g, rc, e);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // used during debug
|
||||
private void labelArticulations(Component component, GraphicsDecorator g,
|
||||
RenderContext<V, E> rc, E e) {
|
||||
|
||||
int offset = 5;
|
||||
int counter = 1;
|
||||
List<Point2D> points = e.getArticulationPoints();
|
||||
for (Point2D p : points) {
|
||||
|
||||
MultiLayerTransformer multiLayerTransformer = rc.getMultiLayerTransformer();
|
||||
p = multiLayerTransformer.transform(Layer.LAYOUT, p);
|
||||
|
||||
EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer();
|
||||
Font font = rc.getEdgeFontTransformer().apply(e);
|
||||
boolean isSelected = rc.getPickedEdgeState().isPicked(e);
|
||||
component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(),
|
||||
"p" + counter++, font, isSelected, e);
|
||||
|
||||
Dimension d = component.getPreferredSize();
|
||||
AffineTransform old = g.getTransform();
|
||||
AffineTransform xform = new AffineTransform(old);
|
||||
xform.translate(p.getX() + offset, p.getY());
|
||||
g.setTransform(xform);
|
||||
g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true);
|
||||
g.setTransform(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,11 +56,17 @@ import ghidra.util.task.TaskMonitor;
|
|||
* <p>Edges returning to the default code flow are painted lighter to de-emphasize them. This
|
||||
* could be made into an option.
|
||||
*
|
||||
* <p>Edge routing herein defaults to 'simple routing'; 'complex routing' is a user option.
|
||||
* Simple routing will reduce edge noise as much as possible by combining/overlapping edges that
|
||||
* flow towards the bottom of the function (returning code flow). Also, edges may fall behind
|
||||
* vertices for some functions. Complex routing allows the user to visually follow the flow
|
||||
* of an individual edge. Complex routing will prevent edges from overlapping and will route
|
||||
* edges around vertices. Simple routing is better when the layout of the vertices is
|
||||
* important to the user; complex routing is better when edges/relationships are more
|
||||
* important to the user.
|
||||
*
|
||||
* TODO ideas:
|
||||
* -paint fallthrough differently for all, or just for those returning to the baseline
|
||||
* -using 'edge routing mode' that avoids vertices can be tweaked so that all returning edges also
|
||||
* will not overlap (do this by keeping some sort of mapping for a given edge's column); when
|
||||
* not routing edges around vertices, the edges intentionally overlap, to reduce noise
|
||||
*/
|
||||
public class DecompilerNestedLayout extends AbstractFGLayout {
|
||||
|
||||
|
@ -265,9 +271,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
Column loopEndColumn = layoutToGridMap.nextColumn(outermostCol);
|
||||
List<Point2D> articulations = routeLoopEdge(start, end, loopEndColumn);
|
||||
newEdgeArticulations.put(e, articulations);
|
||||
|
||||
vertex2dFactory.dispose();
|
||||
return newEdgeArticulations;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,7 +344,11 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
//
|
||||
|
||||
int delta = start.rowIndex - end.rowIndex;
|
||||
int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
int multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
if (useSimpleRouting()) {
|
||||
multiplier = 1; // we allow edges to overlap with 'simple routing'
|
||||
}
|
||||
int distanceSpacing = delta * multiplier;
|
||||
|
||||
// Condensing Note: we have guilty knowledge that our parent class my condense the
|
||||
// vertices and edges towards the center of the graph after we calculate positions.
|
||||
|
@ -375,6 +383,9 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
double y2 = y1;
|
||||
articulations.add(new Point2D.Double(x2, y2));
|
||||
|
||||
Column column = vertex2dFactory.getColumn(x1);
|
||||
routeAroundColumnVertices(start, end, column.index, vertex2dFactory, articulations, x2);
|
||||
|
||||
double x3 = x2;
|
||||
double y3 = end.getBottom() - VERTEX_BORDER_THICKNESS;
|
||||
|
||||
|
@ -386,9 +397,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
y3 = Math.max(y3, endYLimit);
|
||||
articulations.add(new Point2D.Double(x3, y3));
|
||||
|
||||
Column column = vertex2dFactory.getColumn(x1);
|
||||
routeAroundColumnVertices(start, end, column.index, vertex2dFactory, articulations, x3);
|
||||
|
||||
double x4 = end.getX();
|
||||
double y4 = y3;
|
||||
articulations.add(new Point2D.Double(x4, y4)); // point is hidden behind the vertex
|
||||
|
@ -431,7 +439,11 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
//
|
||||
|
||||
int delta = end.rowIndex - start.rowIndex;
|
||||
int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
int multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
if (useSimpleRouting()) {
|
||||
multiplier = 1; // we allow edges to overlap with 'simple routing'
|
||||
}
|
||||
int distanceSpacing = delta * multiplier;
|
||||
|
||||
double x1 = start.getX() - VERTEX_BORDER_THICKNESS; // start at the center
|
||||
|
||||
|
@ -462,7 +474,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
// restrict x from moving past the end vertex center x
|
||||
int edgeOffset = 0;
|
||||
if (getLayoutOptions().useEdgeRoutingAroundVertices()) {
|
||||
if (usesEdgeArticulations()) {
|
||||
// for now, only offset edge lines when we are performing complex routing
|
||||
edgeOffset = EDGE_OFFSET_INCOMING_FROM_LEFT;
|
||||
}
|
||||
|
@ -489,7 +501,11 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
//
|
||||
|
||||
int delta = end.rowIndex - start.rowIndex;
|
||||
int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
int multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
if (useSimpleRouting()) {
|
||||
multiplier = 1; // we allow edges to overlap with 'simple routing'
|
||||
}
|
||||
int distanceSpacing = delta * multiplier;
|
||||
|
||||
double startRightX = start.getRight();
|
||||
double x1 = startRightX - VERTEX_BORDER_THICKNESS; // start at the end
|
||||
|
@ -540,14 +556,15 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
private void routeAroundColumnVertices(Vertex2d start, Vertex2d end, int column,
|
||||
Vertex2dFactory vertex2dFactory, List<Point2D> articulations, double edgeX) {
|
||||
|
||||
if (!getLayoutOptions().useEdgeRoutingAroundVertices()) {
|
||||
if (useSimpleRouting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean goingDown = true;
|
||||
int startRow = start.rowIndex;
|
||||
int endRow = end.rowIndex;
|
||||
|
||||
if (startRow > endRow) { // going upwards
|
||||
goingDown = false;
|
||||
endRow = start.rowIndex;
|
||||
startRow = end.rowIndex;
|
||||
}
|
||||
|
@ -578,15 +595,20 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
}
|
||||
|
||||
double centerX = otherVertex.getX();
|
||||
boolean isLeft = edgeX < centerX;
|
||||
boolean goingLeft = edgeX < centerX;
|
||||
|
||||
// no need to check the 'y' value, as the end vertex is below this one
|
||||
if (!goingDown) {
|
||||
// for now, any time an edge goes up, we route it to the right
|
||||
goingLeft = false;
|
||||
}
|
||||
|
||||
if (isLeft) {
|
||||
if (edgeX >= otherVertex.getLeft()) {
|
||||
// less than center; after the start of the vertex
|
||||
VertexClipper vertexClipper = new VertexClipper(goingLeft, goingDown);
|
||||
|
||||
/*
|
||||
// no need to check the 'y' value, as the end vertex is above/below this one
|
||||
if (vertexClipper.isClippingX(otherVertex, edgeX)) {
|
||||
|
||||
/*
|
||||
|
||||
Must route around this vertex - new points:
|
||||
-p1 - just above the intersection point
|
||||
-p2 - just past the left edge
|
||||
|
@ -599,89 +621,60 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
| | |
|
||||
| '-----'
|
||||
'---.
|
||||
|
|
||||
|
|
||||
*/
|
||||
|
||||
// p1 - same x; y just above vertex
|
||||
double x = edgeX;
|
||||
double y = vertexClipper.getTopOffset(otherVertex, vertexToEdgeOffset);
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// Maybe merge points if they are too close together. Visually, many lines
|
||||
// moving around intersecting vertices looks busy. When the intersecting
|
||||
// vertices are close together, we remove some of the articulations in order to
|
||||
// smooth out the edges.
|
||||
if (articulations.size() > 2) {
|
||||
|
||||
/*
|
||||
The last articulation is the one added before this method was called, which
|
||||
lies just below the intersecting vertex. The articulation before that is
|
||||
the one that is the one that is sending the x value straight into the
|
||||
intersecting vertex. Delete that point as well so that the entire edge is
|
||||
shifted to the outside of the intersecting vertex. This will get repeated
|
||||
for each vertex that is intersecting.
|
||||
*/
|
||||
|
||||
// p1 - same x; y just above vertex
|
||||
double x = edgeX;
|
||||
double y = otherVertex.getTop() - vertexToEdgeOffset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// maybe merge points if they are too close together
|
||||
if (articulations.size() > 2) {
|
||||
Point2D previousArticulation = articulations.get(articulations.size() - 2);
|
||||
int closenessHeight = 50;
|
||||
double previousY = previousArticulation.getY();
|
||||
if (y - previousY < closenessHeight) {
|
||||
// remove our first articulation and the one before that so that
|
||||
// the get merged into on line on the outside of the vertex
|
||||
articulations.remove(articulations.size() - 1);
|
||||
articulations.remove(articulations.size() - 1);
|
||||
Point2D newPrevious = articulations.get(articulations.size() - 1);
|
||||
y = newPrevious.getY();
|
||||
}
|
||||
Point2D previousArticulation = articulations.get(articulations.size() - 2);
|
||||
int closenessHeight = 50;
|
||||
double previousY = previousArticulation.getY();
|
||||
if (vertexClipper.isTooCloseY(y, previousY, closenessHeight)) {
|
||||
articulations.remove(articulations.size() - 1);
|
||||
articulations.remove(articulations.size() - 1);
|
||||
Point2D newPrevious = articulations.get(articulations.size() - 1);
|
||||
y = newPrevious.getY();
|
||||
}
|
||||
|
||||
// p2 - move over; same y
|
||||
int offset = Math.max(vertexToEdgeOffset, distanceSpacing);
|
||||
offset *= exaggerationFactor;
|
||||
x = otherVertex.getLeft() - offset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// p3 - same x; move y below the vertex
|
||||
y = otherVertex.getBottom() + vertexToEdgeOffset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// p4 - move over back to our original x; same y
|
||||
x = edgeX;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
}
|
||||
}
|
||||
else { // right
|
||||
|
||||
double otherEnd = otherVertex.getRight();
|
||||
if (edgeX < otherEnd) {
|
||||
// greater than center; before the end of the vertex
|
||||
// (see not above for routing info)
|
||||
// p2 - move over; same y
|
||||
int offset = Math.max(vertexToEdgeOffset, distanceSpacing);
|
||||
offset *= exaggerationFactor;
|
||||
x = vertexClipper.getSideOffset(otherVertex, offset);
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// p1 - same x; y just above vertex
|
||||
double x = edgeX;
|
||||
double y = otherVertex.getTop() - vertexToEdgeOffset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
// p3 - same x; move y above/below the vertex
|
||||
y = vertexClipper.getBottomOffset(otherVertex, vertexToEdgeOffset);
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// maybe merge points if they are too close together
|
||||
if (articulations.size() > 2) {
|
||||
Point2D previousArticulation = articulations.get(articulations.size() - 2);
|
||||
int closenessHeight = 50;
|
||||
double previousY = previousArticulation.getY();
|
||||
if (y - previousY < closenessHeight) {
|
||||
// remove our first articulation and the one before that so that
|
||||
// the get merged into on line on the outside of the vertex
|
||||
articulations.remove(articulations.size() - 1);
|
||||
articulations.remove(articulations.size() - 1);
|
||||
Point2D newPrevious = articulations.get(articulations.size() - 1);
|
||||
y = newPrevious.getY();
|
||||
}
|
||||
}
|
||||
|
||||
// p2 - move over; same y
|
||||
int offset = Math.max(vertexToEdgeOffset, distanceSpacing);
|
||||
offset *= exaggerationFactor;
|
||||
x = otherEnd + offset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// p3 - same x; move y below the vertex
|
||||
y = otherVertex.getBottom() + vertexToEdgeOffset;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// p4 - move over back to our original x; same y
|
||||
x = edgeX;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
}
|
||||
// p4 - move over back to our original x; same y
|
||||
x = edgeX;
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean useSimpleRouting() {
|
||||
return !getLayoutOptions().useEdgeRoutingAroundVertices();
|
||||
}
|
||||
|
||||
private List<Point2D> routeLoopEdge(Vertex2d start, Vertex2d end, Column loopEndColumn) {
|
||||
|
||||
// going backwards
|
||||
|
@ -691,8 +684,17 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
int halfWidth = loopEndColumn.getPaddedWidth(isCondensedLayout()) >> 1;
|
||||
double x = loopEndColumn.x + halfWidth; // middle of the column
|
||||
|
||||
Point2D startVertexPoint = start.center;
|
||||
int startRow = start.rowIndex;
|
||||
int endRow = end.rowIndex;
|
||||
if (startRow > endRow) { // going upwards
|
||||
endRow = start.rowIndex;
|
||||
startRow = end.rowIndex;
|
||||
}
|
||||
|
||||
int delta = endRow - startRow;
|
||||
x += delta; // adding the delta makes overlap less likely
|
||||
|
||||
Point2D startVertexPoint = start.center;
|
||||
double y1 = startVertexPoint.getY();
|
||||
Point2D first = new Point2D.Double(x, y1);
|
||||
articulations.add(first);
|
||||
|
@ -745,7 +747,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
}
|
||||
|
||||
private void debug(String text) {
|
||||
// System.err.println(text);
|
||||
// System.err.println(text);
|
||||
}
|
||||
|
||||
private void printParts(int depth, BlockGraph block) {
|
||||
|
@ -937,6 +939,59 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
/**
|
||||
* Encapsulates knowledge of edge direction (up/down, left/right) and uses that knowledge
|
||||
* to report vertex offsets from the appropriate side and top/bottom
|
||||
*/
|
||||
private class VertexClipper {
|
||||
|
||||
boolean goingLeft;
|
||||
boolean goingDown;
|
||||
|
||||
VertexClipper(boolean isLeft, boolean isBottom) {
|
||||
this.goingLeft = isLeft;
|
||||
this.goingDown = isBottom;
|
||||
}
|
||||
|
||||
private double getSide(Vertex2d v) {
|
||||
return goingLeft ? v.getLeft() : v.getRight();
|
||||
}
|
||||
|
||||
double getTopOffset(Vertex2d v, int offset) {
|
||||
return goingDown ? v.getTop() - offset : v.getBottom() + offset;
|
||||
}
|
||||
|
||||
double getBottomOffset(Vertex2d v, int offset) {
|
||||
return goingDown ? v.getBottom() + offset : v.getTop() - offset;
|
||||
}
|
||||
|
||||
double getSideOffset(Vertex2d v, int offset) {
|
||||
|
||||
double side = getSide(v);
|
||||
if (goingLeft) {
|
||||
return side - offset;
|
||||
}
|
||||
return side + offset;
|
||||
}
|
||||
|
||||
boolean isTooCloseY(double topY, double bottomY, double threshold) {
|
||||
double delta = goingDown ? topY - bottomY : bottomY - topY;
|
||||
return delta < threshold;
|
||||
}
|
||||
|
||||
boolean isClippingX(Vertex2d v, double x) {
|
||||
|
||||
double side = getSide(v);
|
||||
if (goingLeft) {
|
||||
return x >= side;
|
||||
}
|
||||
return x < side;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating and caching {@link Vertex2d} objects
|
||||
*/
|
||||
private class Vertex2dFactory {
|
||||
|
||||
private VisualGraphVertexShapeTransformer<FGVertex> vertexShaper;
|
||||
|
@ -1216,7 +1271,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return PcodeBlock.typeToName(pcodeBlock.getType()) + " - " + pcodeBlock.getStart();
|
||||
return PcodeBlock.typeToName(pcodeBlock.getType()) + " - " + getName() + " - " +
|
||||
pcodeBlock.getStart();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue