Merge remote-tracking branch 'origin/GP-5717-dragonmacher-vertex-shapes'

This commit is contained in:
Ryan Kurtz 2025-05-27 13:16:18 -04:00
commit 88f77e5cd1
9 changed files with 882 additions and 466 deletions

View file

@ -26,6 +26,8 @@ color.fg.dialog.equates.suggestion = color.palette.hint
color.fg.consoletextpane = color.fg
color.fg.consoletextpane.error = color.fg.error
color.bg.graph.vertex.function = MediumAquamarine
color.fg.infopanel.version = color.fg
color.bg.interpreterconsole = color.bg

View file

@ -0,0 +1,64 @@
/* ###
* 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.base.graph;
import java.awt.Shape;
import javax.swing.JComponent;
import ghidra.graph.viewer.vertex.AbstractVisualVertex;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
/**
* A vertex that is a circle shape with a label below the circle to show the given text.
*/
public class CircleWithLabelVertex extends AbstractVisualVertex implements VertexShapeProvider {
protected CircleWithLabelVertexShapeProvider shapeProvider;
public CircleWithLabelVertex(String label) {
this.shapeProvider = new CircleWithLabelVertexShapeProvider(label);
}
@Override
public JComponent getComponent() {
return shapeProvider.getComponent();
}
@Override
public Shape getCompactShape() {
return shapeProvider.getCompactShape();
}
@Override
public Shape getFullShape() {
return shapeProvider.getFullShape();
}
public String getName() {
return shapeProvider.getName();
}
@Override
public String toString() {
return shapeProvider.getName();// + " @ " + level; // + " (" + System.identityHashCode(this) + ')';
}
@Override
public void dispose() {
// nothing to do
}
}

View file

@ -0,0 +1,359 @@
/* ###
* 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.base.graph;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Ellipse2D.Double;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GDLabel;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
import resources.Icons;
import resources.ResourceManager;
public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
//@formatter:off
public static final Color DEFAULT_VERTEX_SHAPE_COLOR = new GColor("color.bg.graph.vertex.function");
//@formatter:on
protected static final Icon EXPAND_ICON =
ResourceManager.getScaledIcon(Icons.EXPAND_ALL_ICON, 10, 10);
protected static final Icon COLLAPSE_ICON =
ResourceManager.getScaledIcon(Icons.COLLAPSE_ALL_ICON, 10, 10);
// higher numbered layers go on top
protected static final Integer VERTEX_SHAPE_LAYER = 100;
protected static final Integer TOGGLE_BUTTON_LAYER = 200;
protected static final Integer LABEL_LAYER = 300;
protected static final int GAP = 2;
protected static final int VERTEX_SHAPE_SIZE = 50;
// Note: This should be made into an option
// based upon the default function name, plus some extra
protected static final int MAX_NAME_LENGTH = 30;
protected JLayeredPane layeredPane;
protected JButton toggleInsButton = new EmptyBorderButton(EXPAND_ICON);
protected JButton toggleOutsButton = new EmptyBorderButton(EXPAND_ICON);
protected JLabel nameLabel = new GDLabel();
protected JLabel vertexImageLabel = new GDLabel();
protected Double vertexShape;
protected Double compactShape;
protected Shape fullShape;
protected boolean incomingExpanded;
protected boolean outgoingExpanded;
// set this to true to see borders around the components of this vertex
protected boolean useDebugBorders = false;
private String fullLabelText;
public CircleWithLabelVertexShapeProvider(String label) {
this.fullLabelText = label;
buildUi();
}
public CircleWithLabelVertexShapeProvider(String label,
VertexExpansionListener expansionListener) {
this.fullLabelText = label;
buildUi();
}
protected void buildUi() {
String name = generateLabelText();
nameLabel.setText(name);
buildVertexShape();
// calculate the needed size
layeredPane = new JLayeredPane();
Border border = createDebugBorder(new LineBorder(Palette.GOLD, 1));
layeredPane.setBorder(border);
updateLayeredPaneSize();
// layout the components
addVertexShape();
addToggleButtons();
addNameLabel();
buildFullShape();
}
protected String generateLabelText() {
return fullLabelText;
}
private Border createDebugBorder(Border border) {
if (useDebugBorders) {
return border;
}
return BorderFactory.createEmptyBorder();
}
private void buildFullShape() {
// Note: this method assumes all bounds have been set
Area parent = new Area();
Area v = new Area(vertexShape);
Area name = new Area(nameLabel.getBounds());
parent.add(v);
parent.add(name);
// for now, the buttons only appear on hover, but if we want to avoid clipping when
// painting, we need to account for them in the shape's overall bounds
Area in = new Area(toggleInsButton.getBounds());
Area out = new Area(toggleOutsButton.getBounds());
parent.add(in);
parent.add(out);
fullShape = parent;
}
private void updateLayeredPaneSize() {
//
// The overall component size is the total width and height of all components, with any
// spacing between them.
//
Dimension shapeSize = vertexImageLabel.getPreferredSize();
Dimension nameLabelSize = nameLabel.getPreferredSize();
int height = shapeSize.height + GAP + nameLabelSize.height;
Dimension insSize = toggleInsButton.getPreferredSize();
Dimension outsSize = toggleOutsButton.getPreferredSize();
int buttonWidth = Math.max(insSize.width, outsSize.width);
int offset = buttonWidth / 3; // overlap the vertex shape
int width = offset + shapeSize.width;
width = Math.max(width, nameLabelSize.width);
layeredPane.setPreferredSize(new Dimension(width, height));
}
private void buildVertexShape() {
int w = VERTEX_SHAPE_SIZE;
int h = VERTEX_SHAPE_SIZE;
Double circle = new Ellipse2D.Double(0, 0, w, h);
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setPaint(getVertexShapePaint());
g2.fill(circle);
g2.dispose();
vertexShape = circle;
compactShape = (Double) vertexShape.clone();
vertexImageLabel.setIcon(new ImageIcon(image));
Border border = createDebugBorder(new LineBorder(Palette.PINK, 1));
vertexImageLabel.setBorder(border);
}
protected Paint getVertexShapePaint() {
return getDefaultVertexShapeColor();
}
protected Color getDefaultVertexShapeColor() {
return DEFAULT_VERTEX_SHAPE_COLOR;
}
private void addVertexShape() {
Dimension parentSize = layeredPane.getPreferredSize();
Dimension size = vertexImageLabel.getPreferredSize();
// centered
int x = (parentSize.width / 2) - (size.width / 2);
int y = 0;
vertexImageLabel.setBounds(x, y, size.width, size.height);
Dimension shapeSize = vertexShape.getBounds().getSize();
// setFrame() will make sure the shape's x,y values are where they need to be
// for the later 'full shape' creation
vertexShape.setFrame(x, y, shapeSize.width, shapeSize.height);
layeredPane.add(vertexImageLabel, VERTEX_SHAPE_LAYER);
}
private void addNameLabel() {
Border border = createDebugBorder(new LineBorder(Palette.GREEN, 1));
nameLabel.setBorder(border);
// assume the vertex label has been bounded
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = nameLabel.getPreferredSize();
// bottom, centered under the shape
int x = (parentBounds.x + (parentBounds.width / 2)) - (size.width / 2);
int y = parentBounds.y + parentBounds.height + GAP;
nameLabel.setBounds(x, y, size.width, size.height);
layeredPane.add(nameLabel, LABEL_LAYER);
}
private void addToggleButtons() {
// hide the button background
toggleInsButton.setBackground(Palette.NO_COLOR);
toggleOutsButton.setBackground(Palette.NO_COLOR);
// This is needed for Flat Dark theme to work correctly, due to the fact that it wants to
// paint its parent background when the button is opaque. The parent background will get
// painted over any items that lie between the button and the parent.
toggleInsButton.setOpaque(false);
toggleOutsButton.setOpaque(false);
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = toggleInsButton.getPreferredSize();
// upper toggle; upper-left
int x = parentBounds.x - (size.width / 3);
int y = 0;
toggleInsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleInsButton, TOGGLE_BUTTON_LAYER);
// lower toggle; lower-left, lined-up with the vertex shape
size = toggleOutsButton.getPreferredSize();
Dimension vertexSize = parentBounds.getSize();
y = vertexSize.height - size.height;
toggleOutsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleOutsButton, TOGGLE_BUTTON_LAYER);
}
public String getName() {
return fullLabelText;
}
public JButton getIncomingToggleButton() {
return toggleInsButton;
}
public JButton getOutgoingToggleButton() {
return toggleOutsButton;
}
/**
* Sets to true if this vertex is showing all edges in the incoming direction
*
* @param setExpanded true if this vertex is showing all edges in the incoming direction
*/
public void setIncomingExpanded(boolean setExpanded) {
this.incomingExpanded = setExpanded;
toggleInsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " incoming edges");
}
/**
* Returns true if this vertex is showing all edges in the incoming direction
*
* @return true if this vertex is showing all edges in the incoming direction
*/
public boolean isIncomingExpanded() {
return incomingExpanded;
}
/**
* Sets to true if this vertex is showing all edges in the outgoing direction
*
* @param setExpanded true if this vertex is showing all edges in the outgoing direction
*/
public void setOutgoingExpanded(boolean setExpanded) {
this.outgoingExpanded = setExpanded;
toggleOutsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " outgoing edges");
}
/**
* Returns true if this vertex is showing all edges in the outgoing direction
*
* @return true if this vertex is showing all edges in the outgoing direction
*/
public boolean isOutgoingExpanded() {
return outgoingExpanded;
}
/**
* Returns whether this vertex is fully expanded in its current direction
*
* @return whether this vertex is fully expanded in its current direction
*/
public boolean isExpanded() {
return isIncomingExpanded() && isOutgoingExpanded();
}
/**
* Returns true if this node can be expanded
* @return true if this node can be expanded
*/
public boolean canExpand() {
return false; // subclasses can override
}
protected boolean hasIncomingEdges() {
return false;
}
protected boolean hasOutgoingEdges() {
return false;
}
protected void setTogglesVisible(boolean visible) {
toggleInsButton.setVisible(visible);
toggleOutsButton.setVisible(visible);
}
public JComponent getComponent() {
return layeredPane;
}
@Override
public Shape getCompactShape() {
return compactShape;
}
@Override
public Shape getFullShape() {
return fullShape;
}
@Override
public String toString() {
return getName();// + " @ " + level; // + " (" + System.identityHashCode(this) + ')';
}
}

View file

@ -0,0 +1,38 @@
/* ###
* 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.base.graph;
import ghidra.graph.viewer.VisualVertex;
/**
* A listener to know when a vertex has been told to expand
*/
public interface VertexExpansionListener {
/**
* Show or hide those vertices that are on incoming edges to v
*
* @param v the vertex
*/
public void toggleIncomingVertices(VisualVertex v);
/**
* Show or hide those vertices that are on outgoing edges to v
*
* @param v the vertex
*/
public void toggleOutgoingVertices(VisualVertex v);
}

View file

@ -1,9 +1,8 @@
[Defaults]
color.bg.plugin.fcg.vertex.default = MediumAquamarine // TODO
color.bg.plugin.fcg.vertex.toobig = color.palette.lightgray
color.bg.plugin.fcg.edge.primary.direct = darkseagreen // TODO
color.bg.plugin.fcg.edge.primary.direct = darkseagreen
color.bg.plugin.fcg.edge.primary.direct.selected = color.palette.lightgreen
color.bg.plugin.fcg.edge.primary.indirect = color.palette.lavender
color.bg.plugin.fcg.edge.primary.indirect.selected = color.palette.silver

View file

@ -15,88 +15,26 @@
*/
package functioncalls.graph;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Ellipse2D.Double;
import java.awt.image.BufferedImage;
import java.util.Objects;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.JButton;
import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GDLabel;
import functioncalls.plugin.FcgOptions;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui;
import ghidra.graph.viewer.vertex.AbstractVisualVertex;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
import ghidra.base.graph.CircleWithLabelVertex;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.util.StringUtilities;
import resources.Icons;
import resources.ResourceManager;
/**
* A {@link FunctionCallGraph} vertex
*/
public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvider {
//@formatter:off
public static final Color DEFAULT_VERTEX_SHAPE_COLOR = new GColor("color.bg.plugin.fcg.vertex.default");
private static final Color TOO_BIG_VERTEX_SHAPE_COLOR = new GColor("color.bg.plugin.fcg.vertex.toobig");
//@formatter:on
public static final Icon NOT_ALLOWED_ICON = Icons.ERROR_ICON;
private static final Icon EXPAND_ICON =
ResourceManager.getScaledIcon(Icons.EXPAND_ALL_ICON, 10, 10);
private static final Icon COLLAPSE_ICON =
ResourceManager.getScaledIcon(Icons.COLLAPSE_ALL_ICON, 10, 10);
// higher numbered layers go on top
private static final Integer VERTEX_SHAPE_LAYER = 100;
private static final Integer TOGGLE_BUTTON_LAYER = 200;
private static final Integer LABEL_LAYER = 300;
private static final int GAP = 2;
private static final int VERTEX_SHAPE_SIZE = 50;
// TODO to be made an option in an upcoming ticket
// based upon the default function name, plus some extra
private static final int MAX_NAME_LENGTH = 30;
public class FcgVertex extends CircleWithLabelVertex {
private Function function;
private JLayeredPane layeredPane;
private JButton toggleInsButton = new EmptyBorderButton(EXPAND_ICON);
private JButton toggleOutsButton = new EmptyBorderButton(EXPAND_ICON);
private JLabel nameLabel = new GDLabel();
private JLabel vertexImageLabel = new GDLabel();
private Double vertexShape;
private Double compactShape;
private Shape fullShape;
// these values are set after construction from external sources
private boolean hasIncomingReferences;
private boolean hasOutgoingReferences;
private boolean tooManyIncomingReferences;
private boolean tooManyOutgoingReferences;
private boolean incomingExpanded;
private boolean outgoingExpanded;
// set this to true to see borders around the components of this vertex
private boolean useDebugBorders = false;
private Paint inPaint;
private Paint outPaint;
private FcgLevel level;
private FcgOptions options;
private FcgVertexShapeProvider fcgShapeProvider;
/**
* Constructor
*
@ -107,252 +45,14 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
*/
public FcgVertex(Function function, FcgLevel level,
FcgVertexExpansionListener expansionListener, FcgOptions options) {
super(function.getName());
this.function = function;
this.level = level;
this.options = options;
Objects.requireNonNull(expansionListener);
toggleInsButton.addActionListener(e -> {
if (tooManyIncomingReferences) {
return;
}
expansionListener.toggleIncomingVertices(FcgVertex.this);
});
fcgShapeProvider = new FcgVertexShapeProvider(this, expansionListener);
shapeProvider = fcgShapeProvider;
toggleOutsButton.addActionListener(e -> {
if (tooManyOutgoingReferences) {
return;
}
expansionListener.toggleOutgoingVertices(FcgVertex.this);
});
buildUi();
setTogglesVisible(false);
}
private void createPaints() {
Color vertexShapeColor = getVertexShapeColor();
Color lightColor = vertexShapeColor;
Color darkColor = Gui.darker(vertexShapeColor);
Color darkestColor = Gui.darker(darkColor);
int offset = 5 * level.getDistance();
int half = VERTEX_SHAPE_SIZE / 2;
int start = 0;
int end = half + offset;
// paint top-down: dark to light for incoming; light to dark for outgoing
inPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .2f, 1f }, new Color[] { darkestColor, darkColor, lightColor });
start = half - offset; // (offset + 10);
end = VERTEX_SHAPE_SIZE;
outPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .8f, 1f }, new Color[] { lightColor, darkColor, darkestColor });
}
private void buildUi() {
createPaints();
// init the components
boolean truncate = options.useTruncatedFunctionNames();
String name = getName();
if (truncate) {
name = StringUtilities.trimMiddle(getName(), MAX_NAME_LENGTH);
}
nameLabel.setText(name);
buildVertexShape();
// calculate the needed size
layeredPane = new JLayeredPane();
Border border = createDebugBorder(new LineBorder(Palette.GOLD, 1));
layeredPane.setBorder(border);
updateLayeredPaneSize();
// layout the components
addVertexShape();
addToggleButtons();
addNameLabel();
buildFullShape();
}
private Border createDebugBorder(Border border) {
if (useDebugBorders) {
return border;
}
return BorderFactory.createEmptyBorder();
}
private void buildFullShape() {
// Note: this method assumes all bounds have been set
Area parent = new Area();
Area v = new Area(vertexShape);
Area name = new Area(nameLabel.getBounds());
parent.add(v);
parent.add(name);
// for now, the buttons only appear on hover, but if we want to avoid clipping when
// painting, we need to account for them in the shape's overall bounds
Area in = new Area(toggleInsButton.getBounds());
Area out = new Area(toggleOutsButton.getBounds());
parent.add(in);
parent.add(out);
fullShape = parent;
}
private void updateLayeredPaneSize() {
//
// The overall component size is the total width and height of all components, with any
// spacing between them.
//
Dimension shapeSize = vertexImageLabel.getPreferredSize();
Dimension nameLabelSize = nameLabel.getPreferredSize();
int height = shapeSize.height + GAP + nameLabelSize.height;
Dimension insSize = toggleInsButton.getPreferredSize();
Dimension outsSize = toggleOutsButton.getPreferredSize();
int buttonWidth = Math.max(insSize.width, outsSize.width);
int offset = buttonWidth / 3; // overlap the vertex shape
int width = offset + shapeSize.width;
width = Math.max(width, nameLabelSize.width);
layeredPane.setPreferredSize(new Dimension(width, height));
}
private void buildVertexShape() {
int w = VERTEX_SHAPE_SIZE;
int h = VERTEX_SHAPE_SIZE;
Double circle = new Ellipse2D.Double(0, 0, w, h);
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
g2.setColor(getVertexShapeColor());
}
else if (direction.isIn()) {
g2.setPaint(inPaint);
}
else {
g2.setPaint(outPaint);
}
g2.fill(circle);
g2.dispose();
vertexShape = circle;
compactShape = (Double) vertexShape.clone();
vertexImageLabel.setIcon(new ImageIcon(image));
Border border = createDebugBorder(new LineBorder(Palette.PINK, 1));
vertexImageLabel.setBorder(border);
}
private Color getVertexShapeColor() {
if (isInDirection() && tooManyIncomingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
if (isOutDirection() && tooManyOutgoingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
return DEFAULT_VERTEX_SHAPE_COLOR;
}
private boolean isInDirection() {
FcgDirection direction = level.getDirection();
boolean isIn = direction.isIn() || direction.isSource();
return isIn;
}
private boolean isOutDirection() {
FcgDirection direction = level.getDirection();
boolean isOut = direction.isOut() || direction.isSource();
return isOut;
}
private void addVertexShape() {
Dimension parentSize = layeredPane.getPreferredSize();
Dimension size = vertexImageLabel.getPreferredSize();
// centered
int x = (parentSize.width / 2) - (size.width / 2);
int y = 0;
vertexImageLabel.setBounds(x, y, size.width, size.height);
Dimension shapeSize = vertexShape.getBounds().getSize();
// setFrame() will make sure the shape's x,y values are where they need to be
// for the later 'full shape' creation
vertexShape.setFrame(x, y, shapeSize.width, shapeSize.height);
layeredPane.add(vertexImageLabel, VERTEX_SHAPE_LAYER);
}
private void addNameLabel() {
Border border = createDebugBorder(new LineBorder(Palette.GREEN, 1));
nameLabel.setBorder(border);
// assume the vertex label has been bounded
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = nameLabel.getPreferredSize();
// bottom, centered under the shape
int x = (parentBounds.x + (parentBounds.width / 2)) - (size.width / 2);
int y = parentBounds.y + parentBounds.height + GAP;
nameLabel.setBounds(x, y, size.width, size.height);
layeredPane.add(nameLabel, LABEL_LAYER);
}
private void addToggleButtons() {
// hide the button background
toggleInsButton.setBackground(Palette.NO_COLOR);
toggleOutsButton.setBackground(Palette.NO_COLOR);
// This is needed for Flat Dark theme to work correctly, due to the fact that it wants to
// paint its parent background when the button is opaque. The parent background will get
// painted over any items that lie between the button and the parent.
toggleInsButton.setOpaque(false);
toggleOutsButton.setOpaque(false);
Rectangle parentBounds = vertexImageLabel.getBounds();
Dimension size = toggleInsButton.getPreferredSize();
// upper toggle; upper-left
int x = parentBounds.x - (size.width / 3);
int y = 0;
toggleInsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleInsButton, TOGGLE_BUTTON_LAYER);
// lower toggle; lower-left, lined-up with the vertex shape
size = toggleOutsButton.getPreferredSize();
Dimension vertexSize = parentBounds.getSize();
y = vertexSize.height - size.height;
toggleOutsButton.setBounds(x, y, size.width, size.height);
layeredPane.add(toggleOutsButton, TOGGLE_BUTTON_LAYER);
}
public String getName() {
return function.getName();
}
public Function getFunction() {
@ -363,6 +63,10 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
return function.getEntryPoint();
}
public FcgOptions getOptions() {
return options;
}
public FcgLevel getLevel() {
return level;
}
@ -375,105 +79,35 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
return level.getDirection();
}
@Override
public void setHovered(boolean hovered) {
super.setHovered(hovered);
fcgShapeProvider.setTogglesVisible(hovered);
}
public JButton getIncomingToggleButton() {
return toggleInsButton;
return shapeProvider.getIncomingToggleButton();
}
public JButton getOutgoingToggleButton() {
return toggleOutsButton;
return shapeProvider.getOutgoingToggleButton();
}
/**
* Sets to true if this vertex is showing all edges in the incoming direction
* Sets whether this vertex has any incoming references
*
* @param setExpanded true if this vertex is showing all edges in the incoming direction
* @param hasIncoming true if this vertex has any incoming references
*/
public void setIncomingExpanded(boolean setExpanded) {
validateIncomingExpandedState(setExpanded);
this.incomingExpanded = setExpanded;
toggleInsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " incoming edges");
}
private void validateOutgoingExpandedState(boolean isExpanding) {
if (isExpanding) {
if (!canExpandOutgoingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isOutgoingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
}
private void validateIncomingExpandedState(boolean expanding) {
if (expanding) {
if (!canExpandIncomingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isIncomingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
public void setHasIncomingReferences(boolean hasIncoming) {
fcgShapeProvider.setHasIncomingReferences(hasIncoming);
}
/**
* Returns true if this vertex is showing all edges in the incoming direction
*
* @return true if this vertex is showing all edges in the incoming direction
* Sets whether this vertex has any outgoing references
* @param hasOutgoing true if this vertex has any outgoing references
*/
public boolean isIncomingExpanded() {
return incomingExpanded;
}
/**
* Sets to true if this vertex is showing all edges in the outgoing direction
*
* @param setExpanded true if this vertex is showing all edges in the outgoing direction
*/
public void setOutgoingExpanded(boolean setExpanded) {
validateOutgoingExpandedState(setExpanded);
this.outgoingExpanded = setExpanded;
toggleOutsButton.setIcon(setExpanded ? COLLAPSE_ICON : EXPAND_ICON);
String hideShow = setExpanded ? "hide" : "show";
toggleInsButton.setToolTipText("Click to " + hideShow + " outgoing edges");
}
/**
* Returns true if this vertex is showing all edges in the outgoing direction
*
* @return true if this vertex is showing all edges in the outgoing direction
*/
public boolean isOutgoingExpanded() {
return outgoingExpanded;
}
/**
* Returns whether this vertex is fully expanded in its current direction
*
* @return whether this vertex is fully expanded in its current direction
*/
public boolean isExpanded() {
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return isIncomingExpanded() && isOutgoingExpanded();
}
if (direction.isIn()) {
return isIncomingExpanded();
}
return isOutgoingExpanded();
public void setHasOutgoingReferences(boolean hasOutgoing) {
fcgShapeProvider.setHasOutgoingReferences(hasOutgoing);
}
/**
@ -484,10 +118,7 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
* @param tooMany if there are too many references
*/
public void setTooManyIncomingReferences(boolean tooMany) {
this.tooManyIncomingReferences = tooMany;
toggleInsButton.setIcon(NOT_ALLOWED_ICON);
toggleInsButton.setToolTipText("Too many incoming references to show");
buildUi();
fcgShapeProvider.setTooManyIncomingReferences(tooMany);
}
/**
@ -498,10 +129,7 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
* @param tooMany if there are too many references
*/
public void setTooManyOutgoingReferences(boolean tooMany) {
this.tooManyOutgoingReferences = tooMany;
toggleOutsButton.setIcon(NOT_ALLOWED_ICON);
toggleOutsButton.setToolTipText("Too many outgoing references to show");
buildUi();
fcgShapeProvider.setTooManyOutgoingReferences(tooMany);
}
/**
@ -512,7 +140,7 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
* @return true if there are too many references
*/
public boolean hasTooManyIncomingReferences() {
return tooManyIncomingReferences;
return fcgShapeProvider.hasTooManyIncomingReferences();
}
/**
@ -523,7 +151,34 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
* @return true if there are too many references
*/
public boolean hasTooManyOutgoingReferences() {
return tooManyOutgoingReferences;
return fcgShapeProvider.hasTooManyOutgoingReferences();
}
/**
* Returns true if this vertex is showing all edges in the incoming direction
*
* @return true if this vertex is showing all edges in the incoming direction
*/
public boolean isIncomingExpanded() {
return fcgShapeProvider.isIncomingExpanded();
}
/**
* Returns true if this vertex is showing all edges in the outgoing direction
*
* @return true if this vertex is showing all edges in the outgoing direction
*/
public boolean isOutgoingExpanded() {
return fcgShapeProvider.isOutgoingExpanded();
}
/**
* Returns whether this vertex is fully expanded in its current direction
*
* @return whether this vertex is fully expanded in its current direction
*/
public boolean isExpanded() {
return fcgShapeProvider.isExpanded();
}
/**
@ -533,76 +188,33 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
* @return true if this vertex can be expanded
*/
public boolean canExpand() {
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return canExpandIncomingReferences() || canExpandOutgoingReferences();
}
if (direction.isIn()) {
return canExpandIncomingReferences();
}
return canExpandOutgoingReferences();
return fcgShapeProvider.canExpand();
}
public boolean canExpandIncomingReferences() {
return hasIncomingReferences && !tooManyIncomingReferences && !incomingExpanded;
return fcgShapeProvider.canExpandIncomingReferences();
}
public boolean canExpandOutgoingReferences() {
return hasOutgoingReferences && !tooManyOutgoingReferences && !outgoingExpanded;
return fcgShapeProvider.canExpandOutgoingReferences();
}
/**
* Sets whether this vertex has any incoming references
* Sets to true if this vertex is showing all edges in the incoming direction
*
* @param hasIncoming true if this vertex has any incoming references
* @param setExpanded true if this vertex is showing all edges in the incoming direction
*/
public void setHasIncomingReferences(boolean hasIncoming) {
this.hasIncomingReferences = hasIncoming;
public void setIncomingExpanded(boolean setExpanded) {
fcgShapeProvider.setIncomingExpanded(setExpanded);
}
/**
* Sets whether this vertex has any outgoing references
* Sets to true if this vertex is showing all edges in the outgoing direction
*
* @param hasOutgoing true if this vertex has any outgoing references
* @param setExpanded true if this vertex is showing all edges in the outgoing direction
*/
public void setHasOutgoingReferences(boolean hasOutgoing) {
this.hasOutgoingReferences = hasOutgoing;
}
@Override
public void setHovered(boolean hovered) {
super.setHovered(hovered);
setTogglesVisible(hovered);
}
private void setTogglesVisible(boolean visible) {
boolean isIn = isInDirection();
boolean turnOn = isIn && hasIncomingReferences && visible;
toggleInsButton.setVisible(turnOn);
boolean isOut = isOutDirection();
turnOn = isOut && hasOutgoingReferences && visible;
toggleOutsButton.setVisible(turnOn);
}
@Override
public JComponent getComponent() {
return layeredPane;
}
@Override
public Shape getCompactShape() {
return compactShape;
}
@Override
public Shape getFullShape() {
return fullShape;
public void setOutgoingExpanded(boolean setExpanded) {
fcgShapeProvider.setOutgoingExpanded(setExpanded);
}
@Override
@ -633,6 +245,7 @@ public class FcgVertex extends AbstractVisualVertex implements VertexShapeProvid
@Override
public void dispose() {
// nothing to do
this.function = null;
super.dispose();
}
}

View file

@ -0,0 +1,338 @@
/* ###
* 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 functioncalls.graph;
import java.awt.*;
import java.util.Objects;
import javax.swing.Icon;
import functioncalls.plugin.FcgOptions;
import functioncalls.plugin.FunctionCallGraphPlugin;
import generic.theme.GColor;
import generic.theme.Gui;
import ghidra.base.graph.CircleWithLabelVertexShapeProvider;
import ghidra.util.StringUtilities;
import resources.Icons;
/**
* A vertex shape provider for the {@link FunctionCallGraphPlugin}.
*/
public class FcgVertexShapeProvider extends CircleWithLabelVertexShapeProvider {
//@formatter:off
private static final Color TOO_BIG_VERTEX_SHAPE_COLOR = new GColor("color.bg.plugin.fcg.vertex.toobig");
//@formatter:on
public static final Icon NOT_ALLOWED_ICON = Icons.ERROR_ICON;
private FcgVertex vertex;
private Paint inPaint;
private Paint outPaint;
// these values are set after construction from external sources
private boolean hasIncomingReferences;
private boolean hasOutgoingReferences;
private boolean tooManyIncomingReferences;
private boolean tooManyOutgoingReferences;
public FcgVertexShapeProvider(FcgVertex vertex,
FcgVertexExpansionListener expansionListener) {
super(vertex.getName());
this.vertex = vertex;
Objects.requireNonNull(expansionListener);
toggleInsButton.addActionListener(e -> {
if (tooManyIncomingReferences) {
return;
}
expansionListener.toggleIncomingVertices(vertex);
});
toggleOutsButton.addActionListener(e -> {
if (tooManyOutgoingReferences) {
return;
}
expansionListener.toggleOutgoingVertices(vertex);
});
buildUi();
setTogglesVisible(false);
}
private void createPaints() {
Color vertexShapeColor = getDefaultVertexShapeColor();
Color lightColor = vertexShapeColor;
Color darkColor = Gui.darker(vertexShapeColor);
Color darkestColor = Gui.darker(darkColor);
FcgLevel level = vertex.getLevel();
int offset = 5 * level.getDistance();
int half = VERTEX_SHAPE_SIZE / 2;
int start = 0;
int end = half + offset;
// paint top-down: dark to light for incoming; light to dark for outgoing
inPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .2f, 1f }, new Color[] { darkestColor, darkColor, lightColor });
start = half - offset; // (offset + 10);
end = VERTEX_SHAPE_SIZE;
outPaint = new LinearGradientPaint(new Point(0, start), new Point(0, end),
new float[] { .0f, .8f, 1f }, new Color[] { lightColor, darkColor, darkestColor });
}
@Override
protected void buildUi() {
if (vertex == null) {
return; // still being constructed
}
createPaints();
super.buildUi();
}
@Override
protected String generateLabelText() {
FcgOptions options = vertex.getOptions();
boolean optionsUseTruncatedName = options.useTruncatedFunctionNames();
String name = getName();
if (optionsUseTruncatedName) {
name = StringUtilities.trimMiddle(getName(), MAX_NAME_LENGTH);
}
return name;
}
@Override
protected Paint getVertexShapePaint() {
FcgLevel level = vertex.getLevel();
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return getDefaultVertexShapeColor();
}
else if (direction.isIn()) {
return inPaint;
}
else {
return outPaint;
}
}
@Override
protected Color getDefaultVertexShapeColor() {
if (isInDirection() && tooManyIncomingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
if (isOutDirection() && tooManyOutgoingReferences) {
return TOO_BIG_VERTEX_SHAPE_COLOR;
}
return DEFAULT_VERTEX_SHAPE_COLOR;
}
private boolean isInDirection() {
FcgLevel level = vertex.getLevel();
FcgDirection direction = level.getDirection();
boolean isIn = direction.isIn() || direction.isSource();
return isIn;
}
private boolean isOutDirection() {
FcgLevel level = vertex.getLevel();
FcgDirection direction = level.getDirection();
boolean isOut = direction.isOut() || direction.isSource();
return isOut;
}
/**
* Sets to true if this vertex is showing all edges in the incoming direction
* @param setExpanded true if this vertex is showing all edges in the incoming direction
*/
@Override
public void setIncomingExpanded(boolean setExpanded) {
validateIncomingExpandedState(setExpanded);
super.setIncomingExpanded(setExpanded);
}
private void validateOutgoingExpandedState(boolean isExpanding) {
if (isExpanding) {
if (!canExpandOutgoingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isOutgoingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
}
private void validateIncomingExpandedState(boolean expanding) {
if (expanding) {
if (!canExpandIncomingReferences()) {
throw new IllegalStateException("Vertex cannot be expanded: " + this);
}
return;
}
// collapsing
if (!isIncomingExpanded()) {
throw new IllegalStateException("Vertex cannot be collapsed: " + this);
}
}
/**
* Sets to true if this vertex is showing all edges in the outgoing direction
* @param setExpanded true if this vertex is showing all edges in the outgoing direction
*/
@Override
public void setOutgoingExpanded(boolean setExpanded) {
validateOutgoingExpandedState(setExpanded);
super.setOutgoingExpanded(setExpanded);
}
/**
* Returns whether this vertex is fully expanded in its current direction
* @return whether this vertex is fully expanded in its current direction
*/
@Override
public boolean isExpanded() {
FcgLevel level = vertex.getLevel();
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return isIncomingExpanded() && isOutgoingExpanded();
}
if (direction.isIn()) {
return isIncomingExpanded();
}
return isOutgoingExpanded();
}
/**
* Sets whether this vertex has too many incoming references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @param tooMany if there are too many references
*/
public void setTooManyIncomingReferences(boolean tooMany) {
this.tooManyIncomingReferences = tooMany;
toggleInsButton.setIcon(NOT_ALLOWED_ICON);
toggleInsButton.setToolTipText("Too many incoming references to show");
buildUi();
}
/**
* Sets whether this vertex has too many outgoing references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @param tooMany if there are too many references
*/
public void setTooManyOutgoingReferences(boolean tooMany) {
this.tooManyOutgoingReferences = tooMany;
toggleOutsButton.setIcon(NOT_ALLOWED_ICON);
toggleOutsButton.setToolTipText("Too many outgoing references to show");
buildUi();
}
/**
* Returns whether this vertex has too many incoming references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @return true if there are too many references
*/
public boolean hasTooManyIncomingReferences() {
return tooManyIncomingReferences;
}
/**
* Returns whether this vertex has too many outgoing references, where too many is subjectively
* defined by this class. Too many nodes in the display would ruin rendering and general
* usability.
*
* @return true if there are too many references
*/
public boolean hasTooManyOutgoingReferences() {
return tooManyOutgoingReferences;
}
/**
* Returns true if this vertex can expand itself in its current direction, or in either
* direction if this is a source vertex
*
* @return true if this vertex can be expanded
*/
@Override
public boolean canExpand() {
FcgLevel level = vertex.getLevel();
FcgDirection direction = level.getDirection();
if (direction.isSource()) {
return canExpandIncomingReferences() || canExpandOutgoingReferences();
}
if (direction.isIn()) {
return canExpandIncomingReferences();
}
return canExpandOutgoingReferences();
}
public boolean canExpandIncomingReferences() {
return hasIncomingReferences && !tooManyIncomingReferences && !incomingExpanded;
}
public boolean canExpandOutgoingReferences() {
return hasOutgoingReferences && !tooManyOutgoingReferences && !outgoingExpanded;
}
/**
* Sets whether this vertex has any incoming references
*
* @param hasIncoming true if this vertex has any incoming references
*/
public void setHasIncomingReferences(boolean hasIncoming) {
this.hasIncomingReferences = hasIncoming;
}
/**
* Sets whether this vertex has any outgoing references
* @param hasOutgoing true if this vertex has any outgoing references
*/
public void setHasOutgoingReferences(boolean hasOutgoing) {
this.hasOutgoingReferences = hasOutgoing;
}
@Override
protected void setTogglesVisible(boolean visible) {
boolean isIn = isInDirection();
boolean turnOn = isIn && hasIncomingReferences && visible;
toggleInsButton.setVisible(turnOn);
boolean isOut = isOutDirection();
turnOn = isOut && hasOutgoingReferences && visible;
toggleOutsButton.setVisible(turnOn);
}
}

View file

@ -4,9 +4,9 @@
* 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.
@ -21,6 +21,7 @@ import functioncalls.graph.renderer.FcgEdgePaintTransformer;
import functioncalls.graph.renderer.FcgVertexPaintTransformer;
import functioncalls.plugin.FunctionCallGraphPlugin;
import generic.theme.GColor;
import ghidra.base.graph.CircleWithLabelVertexShapeProvider;
import ghidra.graph.viewer.*;
import ghidra.graph.viewer.edge.VisualEdgeRenderer;
import ghidra.graph.viewer.layout.VisualGraphLayout;
@ -33,7 +34,8 @@ import ghidra.graph.viewer.vertex.VisualVertexRenderer;
public class FcgComponent extends GraphComponent<FcgVertex, FcgEdge, FunctionCallGraph> {
private FcgVertexPaintTransformer vertexPaintTransformer =
new FcgVertexPaintTransformer(FcgVertex.DEFAULT_VERTEX_SHAPE_COLOR);
new FcgVertexPaintTransformer(
CircleWithLabelVertexShapeProvider.DEFAULT_VERTEX_SHAPE_COLOR);
private FcgEdgePaintTransformer edgePaintTransformer =
new FcgEdgePaintTransformer(new GColor("color.bg.plugin.fcg.edge.primary.direct"),

View file

@ -4,9 +4,9 @@
* 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.
@ -37,6 +37,7 @@ public abstract class AbstractVisualVertex implements VisualVertex {
this.focused = focused;
}
@Override
public boolean isFocused() {
return focused;
}