Fixes for GP-377, GP-554, GP-551, GP-552, GP-553.

This commit removes previous hacks for overlapping vertices, integrates a configurable graph mouse model, puts back the RTree spatial data structures.
This commit is contained in:
Tom Nelson 2021-01-13 12:44:04 +00:00 committed by dragonmacher
parent 5c98e46d62
commit a4dbc30047
23 changed files with 320 additions and 209 deletions

View file

@ -147,10 +147,10 @@ public class GraphDisplayBrokerPlugin extends Plugin
}
@Override
public GraphDisplay getDefaultGraphDisplay(boolean reuseGraph,
public GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, Map<String, String> properties,
TaskMonitor monitor) throws GraphException {
if (defaultGraphDisplayProvider != null) {
return defaultGraphDisplayProvider.getGraphDisplay(reuseGraph, monitor);
return defaultGraphDisplayProvider.getGraphDisplay(reuseGraph, properties, monitor);
}
return null;
}

View file

@ -15,7 +15,9 @@
*/
package ghidra.app.services;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import ghidra.app.plugin.core.graph.GraphDisplayBrokerListener;
import ghidra.app.plugin.core.graph.GraphDisplayBrokerPlugin;
@ -60,7 +62,12 @@ public interface GraphDisplayBroker {
* @return a {@link GraphDisplay} object to sends graphs to be displayed or exported.
* @throws GraphException thrown if an error occurs trying to get a graph display
*/
public GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, TaskMonitor monitor)
default GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, TaskMonitor monitor)
throws GraphException {
return getDefaultGraphDisplay(reuseGraph, Collections.emptyMap(), monitor);
}
GraphDisplay getDefaultGraphDisplay(boolean reuseGraph, Map<String, String> properties, TaskMonitor monitor)
throws GraphException;
/**

View file

@ -30,6 +30,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.model.pcode.*;
import ghidra.service.graph.*;
import ghidra.util.Msg;
import java.util.*;
public class GraphAST extends GhidraScript {
protected static final String COLOR_ATTRIBUTE = "Color";
@ -64,8 +65,14 @@ public class GraphAST extends GhidraScript {
graph = new AttributedGraph();
buildGraph();
Map<String, String> properties = new HashMap<>();
properties.put("selectedVertexColor", "0xFF1493");
properties.put("selectedEdgeColor", "0xFF1493");
properties.put("initialLayoutAlgorithm", "Hierarchical MinCross Coffman Graham");
properties.put("displayVerticesAsIcons", "false");
properties.put("vertexLabelPosition", "S");
GraphDisplay graphDisplay =
graphDisplayBroker.getDefaultGraphDisplay(false, monitor);
graphDisplayBroker.getDefaultGraphDisplay(false, properties, monitor);
// graphDisplay.defineVertexAttribute(CODE_ATTRIBUTE); //
// graphDisplay.defineVertexAttribute(SYMBOLS_ATTRIBUTE);
// graphDisplay.defineEdgeAttribute(EDGE_TYPE_ATTRIBUTE);

View file

@ -15,7 +15,9 @@
*/
package ghidra.app.plugin.core.decompile.actions;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import docking.widgets.EventTrigger;
import ghidra.app.services.GraphDisplayBroker;
@ -101,7 +103,11 @@ public class ASTGraphTask extends Task {
else {
createControlFlowGraph(graph, monitor);
}
GraphDisplay display = graphService.getDefaultGraphDisplay(!newGraph, monitor);
Map<String, String> properties = new HashMap<>();
properties.put("selectedVertexColor", "0xFF1493");
properties.put("selectedEdgeColor", "0xFF1493");
properties.put("initialLayoutAlgorithm", "Hierarchical MinCross Coffman Graham");
GraphDisplay display = graphService.getDefaultGraphDisplay(!newGraph, properties, monitor);
ASTGraphDisplayListener displayListener =
new ASTGraphDisplayListener(tool, display, hfunction, graphType);
display.setGraphDisplayListener(displayListener);

View file

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

View file

@ -87,6 +87,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
private Logger log = Logger.getLogger(DefaultGraphDisplay.class.getName());
private Map<String, String> graphDisplayProperties = new HashMap<>();
private GraphDisplayListener listener = new DummyGraphDisplayListener();
private String title;
@ -164,15 +165,18 @@ public class DefaultGraphDisplay implements GraphDisplay {
private PopupRegulator<AttributedVertex, AttributedEdge> popupRegulator;
private GhidraGraphCollapser graphCollapser;
private Set<DockingAction> addedActions = new LinkedHashSet<>();
/**
* Create the initial display, the graph-less visualization viewer, and its controls
* @param displayProvider provides a {@link PluginTool} for Docking features
* @param id the unique display id
*/
DefaultGraphDisplay(DefaultGraphDisplayProvider displayProvider, int id) {
DefaultGraphDisplay(DefaultGraphDisplayProvider displayProvider, Map<String, String> graphDisplayProperties, int id) {
this.graphDisplayProvider = displayProvider;
this.displayId = id;
this.pluginTool = graphDisplayProvider.getPluginTool();
this.graphDisplayProperties = graphDisplayProperties;
this.viewer = createViewer();
buildHighlighers();
@ -206,6 +210,28 @@ public class DefaultGraphDisplay implements GraphDisplay {
connectSelectionStateListeners();
}
public void setProperty(String key, String value) {
this.graphDisplayProperties.put(key, value);
}
public String getProperty(String key) {
return graphDisplayProperties.get(key);
}
public Map<String, String> getPropertyMap() {
return Collections.unmodifiableMap(graphDisplayProperties);
}
private Color getSelectedVertexColor() {
return Colors.getHexColor(graphDisplayProperties.getOrDefault("selectedVertexColor",
"0xFF0000"));
}
private Color getSelectedEdgeColor() {
return Colors.getHexColor(graphDisplayProperties.getOrDefault("selectedEdgeColor", "0xFF0000"));
}
JComponent getComponent() {
JComponent component = viewer.getComponent();
component.setFocusable(true);
@ -234,7 +260,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
// this lens' delegate is the viewer's VIEW layer
.delegate(transformer)
.build();
LensGraphMouse lensGraphMouse = new DefaultLensGraphMouse<>(magnificationPlugin);
LensGraphMouse lensGraphMouse = DefaultLensGraphMouse.builder().magnificationPlugin(magnificationPlugin).build();
return MagnifyImageLensSupport.builder(viewer)
.lensTransformer(shapeTransformer)
.lensGraphMouse(lensGraphMouse)
@ -249,7 +275,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
MultiSelectedVertexPaintable<AttributedVertex, AttributedEdge> multiSelectedVertexPaintable =
MultiSelectedVertexPaintable.builder(viewer)
.selectionStrokeMin(4.f)
.selectionPaint(Color.red)
.selectionPaint(getSelectedVertexColor())
.useBounds(false)
.build();
@ -257,7 +283,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
SingleSelectedVertexPaintable<AttributedVertex, AttributedEdge> singleSelectedVertexPaintable =
SingleSelectedVertexPaintable.builder(viewer)
.selectionStrokeMin(4.f)
.selectionPaint(Color.red)
.selectionPaint(getSelectedVertexColor())
.selectedVertexFunction(vs -> this.focusedVertex)
.build();
@ -375,14 +401,20 @@ public class DefaultGraphDisplay implements GraphDisplay {
.popupMenuPath("Go To Edge Source")
.popupMenuGroup("Go To")
.withContext(EdgeGraphActionContext.class)
.onAction(c -> setFocusedVertex(graph.getEdgeSource(c.getClickedEdge())))
.onAction(c -> {
selectEdge(c.getClickedEdge());
setFocusedVertex(graph.getEdgeSource(c.getClickedEdge()));
})
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Edge Target", ACTION_OWNER)
.popupMenuPath("Go To Edge Target")
.popupMenuGroup("Go To")
.withContext(EdgeGraphActionContext.class)
.onAction(c -> setFocusedVertex(graph.getEdgeTarget(c.getClickedEdge())))
.onAction(c -> {
selectEdge(c.getClickedEdge());
setFocusedVertex(graph.getEdgeTarget(c.getClickedEdge()));
})
.buildAndInstallLocal(componentProvider);
hideSelectedAction = new ToggleActionBuilder("Hide Selected", ACTION_OWNER)
@ -426,6 +458,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
.onAction(c -> growSelection(getSourceVerticesFromSelected()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Grow Selection To Entire Component", ACTION_OWNER)
.popupMenuPath("Grow Selection To Entire Component")
.popupMenuGroup("z", "4")
.description("Extends the current selection by including the target/source vertices " +
"of all edges whose source/target is selected")
.keyBinding("ctrl C")
.enabledWhen(c -> !isAllSelected(getSourceVerticesFromSelected()) && !isAllSelected(getTargetVerticesFromSelected()))
.onAction(c -> growSelection(getAllComponentVerticesFromSelected()))
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Clear Selection", ACTION_OWNER)
.popupMenuPath("Clear Selection")
.popupMenuGroup("z", "5")
@ -438,7 +481,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
.popupMenuPath("Display Selected as New Graph")
.popupMenuGroup("zz", "5")
.description("Creates a subgraph from the selected nodes")
.enabledWhen(c -> !viewer.getSelectedVertexState().getSelected().isEmpty())
.enabledWhen(c -> !viewer.getSelectedVertices().isEmpty())
.onAction(c -> createAndDisplaySubGraph())
.buildAndInstallLocal(componentProvider);
@ -505,16 +548,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private boolean hasSelection() {
return !(viewer.getSelectedVertexState().getSelected().isEmpty() &&
viewer.getSelectedEdgeState().getSelected().isEmpty());
return !(viewer.getSelectedVertices().isEmpty() &&
viewer.getSelectedEdges().isEmpty());
}
private boolean isSelected(AttributedVertex v) {
return viewer.getSelectedVertexState().isSelected(v);
return viewer.getSelectedVertices().contains(v);
}
private boolean isSelected(AttributedEdge e) {
return viewer.getSelectedEdgeState().isSelected(e);
return viewer.getSelectedEdges().contains(e);
}
private void createAndDisplaySubGraph() {
@ -522,6 +565,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
try {
display.setGraph(createSubGraph(), "SubGraph", false, TaskMonitor.DUMMY);
display.setGraphDisplayListener(listener.cloneWith(display));
addedActions.forEach(display::addAction);
}
catch (CancelledException e) {
// using Dummy, so can't happen
@ -529,7 +573,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private AttributedGraph createSubGraph() {
Set<AttributedVertex> selected = viewer.getSelectedVertexState().getSelected();
Set<AttributedVertex> selected = viewer.getSelectedVertices();
Graph<AttributedVertex, AttributedEdge> subGraph = new AsSubgraph<>(graph, selected);
AttributedGraph newGraph = new AttributedGraph();
@ -547,7 +591,23 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private boolean isAllSelected(Set<AttributedVertex> vertices) {
return viewer.getSelectedVertexState().getSelected().containsAll(vertices);
return viewer.getSelectedVertices().containsAll(vertices);
}
private Set<AttributedVertex> getSourceVerticesFromSelected() {
Set<AttributedVertex> sources = new HashSet<>();
Set<AttributedVertex> selectedVertices = getSelectedVertices();
selectedVertices.forEach(v -> {
Set<AttributedEdge> edges = graph.incomingEdgesOf(v);
edges.forEach(e -> sources.add(graph.getEdgeSource(e)));
});
return sources;
}
private Set<AttributedVertex> getUnselectedSourceVerticesFromSelected() {
MutableSelectedState<AttributedVertex> selectedVertexState = viewer.getSelectedVertexState();
return getSourceVerticesFromSelected().stream()
.filter(v -> !selectedVertexState.isSelected(v)).collect(Collectors.toSet());
}
private Set<AttributedVertex> getTargetVerticesFromSelected() {
@ -560,16 +620,43 @@ public class DefaultGraphDisplay implements GraphDisplay {
return targets;
}
private Set<AttributedVertex> getSourceVerticesFromSelected() {
Set<AttributedVertex> sources = new HashSet<>();
Set<AttributedVertex> selectedVertices = getSelectedVertices();
selectedVertices.forEach(v -> {
Set<AttributedEdge> edges = graph.incomingEdgesOf(v);
edges.forEach(e -> sources.add(graph.getEdgeSource(e)));
});
return sources;
private Set<AttributedVertex> getUnselectedTargetVerticesFromSelected() {
MutableSelectedState<AttributedVertex> selectedVertexState = viewer.getSelectedVertexState();
return getTargetVerticesFromSelected().stream()
.filter(v -> !selectedVertexState.isSelected(v)).collect(Collectors.toSet());
}
private Set<AttributedVertex> getAllDownstreamVerticesFromSelected() {
MutableSelectedState<AttributedVertex> selectedVertexState = viewer.getSelectedVertexState();
Set<AttributedVertex> downstream = new HashSet<>();
Set<AttributedVertex> targets = getUnselectedTargetVerticesFromSelected();
while (!targets.isEmpty()) {
downstream.addAll(targets);
growSelection(targets);
targets = getUnselectedTargetVerticesFromSelected();
}
return downstream;
}
private Set<AttributedVertex> getAllUpstreamVerticesFromSelected() {
MutableSelectedState<AttributedVertex> selectedVertexState = viewer.getSelectedVertexState();
Set<AttributedVertex> upstream = new HashSet<>();
Set<AttributedVertex> sources = getUnselectedSourceVerticesFromSelected();
while (!sources.isEmpty()) {
growSelection(sources);
upstream.addAll(sources);
sources = getUnselectedSourceVerticesFromSelected();
}
return upstream;
}
public Set<AttributedVertex> getAllComponentVerticesFromSelected() {
Set<AttributedVertex> componentVertices = getAllDownstreamVerticesFromSelected();
componentVertices.addAll(getAllUpstreamVerticesFromSelected());
return componentVertices;
}
private void invertSelection() {
switchableSelectionListener.setEnabled(false);
try {
@ -659,11 +746,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
SatelliteVisualizationViewer.builder(parentViewer)
.viewSize(satelliteSize)
.build();
satellite.setGraphMouse(new DefaultSatelliteGraphMouse());
satellite.setGraphMouse(new DefaultSatelliteGraphMouse<>());
satellite.getRenderContext().setEdgeDrawPaintFunction(Colors::getColor);
satellite.getRenderContext()
.setEdgeStrokeFunction(ProgramGraphFunctions::getEdgeStroke);
satellite.getRenderContext().setVertexFillPaintFunction(Colors::getColor);
satellite.getRenderContext()
.setEdgeDrawPaintFunction(viewer.getRenderContext().getEdgeDrawPaintFunction());
satellite.getRenderContext()
.setVertexFillPaintFunction(viewer.getRenderContext().getVertexFillPaintFunction());
satellite.getRenderContext()
.setVertexDrawPaintFunction(viewer.getRenderContext().getVertexDrawPaintFunction());
satellite.scaleToLayout();
satellite.getRenderContext().setVertexLabelFunction(n -> null);
// always get the current predicate from the main view and test with it,
@ -834,12 +927,21 @@ public class DefaultGraphDisplay implements GraphDisplay {
// set the graph but defer the layout algorithm setting
viewer.getVisualizationModel().setGraph(graph, false);
configureFilters();
setInitialLayoutAlgorithm();
});
componentProvider.setVisible(true);
}
private void setInitialLayoutAlgorithm() {
if (graphDisplayProperties.containsKey("initialLayoutAlgorithm")) {
String layoutAlgorithmName = graphDisplayProperties.get("initialLayoutAlgorithm");
layoutTransitionManager.setLayout(layoutAlgorithmName);
} else {
LayoutAlgorithm<AttributedVertex> initialLayoutAlgorithm =
layoutTransitionManager.getInitialLayoutAlgorithm();
initialLayoutAlgorithm.setAfter(() -> centerAndScale());
viewer.getVisualizationModel().setLayoutAlgorithm(initialLayoutAlgorithm);
});
componentProvider.setVisible(true);
}
}
/**
@ -935,6 +1037,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
public void setVertexLabel(String attributeName, int alignment, int size, boolean monospace,
int maxLines) {
log.fine("setVertexLabel " + attributeName);
this.iconCache.setPreferredLabel(attributeName);
// this would have to set the label function, the label font function
}
@ -1075,6 +1178,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
* @return the new VisualizationViewer
*/
public VisualizationViewer<AttributedVertex, AttributedEdge> createViewer() {
Color selectedVertexColor =
Colors.getHexColor(graphDisplayProperties.getOrDefault("selectedVertexColor", "0xFF0000"));
Color selectedEdgeColor =
Colors.getHexColor(graphDisplayProperties.getOrDefault("selectedEdgeColor", "0xFF0000"));
final VisualizationViewer<AttributedVertex, AttributedEdge> vv =
VisualizationViewer.<AttributedVertex, AttributedEdge> builder()
.multiSelectionStrategySupplier(
@ -1119,22 +1226,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
this.iconCache = new GhidraIconCache();
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
// set up the shape and color functions
IconShapeFunction<AttributedVertex> nodeImageShapeFunction =
new IconShapeFunction<>(new EllipseShapeFunction<>());
vv.getRenderContext().setVertexIconFunction(iconCache::get);
// cause the vertices to be drawn with custom icons/shapes
nodeImageShapeFunction.setIconFunction(iconCache::get);
renderContext.setVertexShapeFunction(nodeImageShapeFunction);
renderContext.setVertexIconFunction(iconCache::get);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(
nodeImageShapeFunction.andThen(s -> RectangleUtils.convert(s.getBounds2D())))
.build());
setVertexPreferences(vv);
// the selectedEdgeState will be controlled by the vertices that are selected.
// if both endpoints of an edge are selected, select that edge.
vv.setSelectedEdgeState(
@ -1143,17 +1235,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
// selected edges will be drawn with a wider stroke
renderContext.setEdgeStrokeFunction(
e -> renderContext.getSelectedEdgeState().isSelected(e) ? new BasicStroke(20.f)
e -> vv.getSelectedEdges().contains(e) ? new BasicStroke(20.f)
: ProgramGraphFunctions.getEdgeStroke(e));
// selected edges will be drawn in red (instead of default)
renderContext.setEdgeDrawPaintFunction(
e -> renderContext.getSelectedEdgeState().isSelected(e) ? Color.red
e -> vv.getSelectedEdges().contains(e) ? selectedEdgeColor
: Colors.getColor(e));
renderContext.setArrowDrawPaintFunction(
e -> renderContext.getSelectedEdgeState().isSelected(e) ? Color.red
e -> vv.getSelectedEdges().contains(e) ? selectedEdgeColor
: Colors.getColor(e));
renderContext.setArrowFillPaintFunction(
e -> renderContext.getSelectedEdgeState().isSelected(e) ? Color.red
e -> vv.getSelectedEdges().contains(e) ? selectedEdgeColor
: Colors.getColor(e));
// assign the shapes to the modal renderer
@ -1190,6 +1282,35 @@ public class DefaultGraphDisplay implements GraphDisplay {
return vv;
}
private void setVertexPreferences(VisualizationViewer<AttributedVertex, AttributedEdge> vv) {
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
if (Boolean.parseBoolean(graphDisplayProperties.getOrDefault("displayVerticesAsIcons", "true"))) {
// set up the shape and color functions
IconShapeFunction<AttributedVertex> nodeImageShapeFunction =
new IconShapeFunction<>(new EllipseShapeFunction<>());
nodeImageShapeFunction.setIconFunction(iconCache::get);
renderContext.setVertexShapeFunction(nodeImageShapeFunction);
renderContext.setVertexIconFunction(iconCache::get);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(
nodeImageShapeFunction.andThen(s -> RectangleUtils.convert(s.getBounds2D())))
.build());
} else {
vv.getRenderContext().setVertexShapeFunction(ProgramGraphFunctions::getVertexShape);
vv.setInitialDimensionFunction(InitialDimensionFunction
.builder(
renderContext.getVertexShapeFunction()
.andThen(s -> RectangleUtils.convert(s.getBounds2D())))
.build());
vv.getRenderContext().setVertexLabelFunction(Object::toString);
vv.getRenderContext().setVertexLabelPosition(
VertexLabel.Position.valueOf(
graphDisplayProperties.getOrDefault("vertexLabelPosition", "AUTO")));
}
}
/**
* Item listener for selection changes in the graph with the additional
* capability of being able to disable the listener without removing it.
@ -1236,6 +1357,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public void addAction(DockingAction action) {
addedActions.add(action);
Swing.runLater(() -> componentProvider.addLocalAction(action));
}
@ -1246,7 +1368,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public Set<AttributedVertex> getSelectedVertices() {
return viewer.getSelectedVertexState().getSelected();
return viewer.getSelectedVertices();
}
public ActionContext getActionContext(MouseEvent e) {
@ -1314,6 +1436,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
Swing.runLater(() -> {
// remove all actions
componentProvider.removeAllLocalActions();
addedActions.clear();
// put the standard graph actions back
createToolbarActions();
createPopupActions();

View file

@ -15,7 +15,9 @@
*/
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;
@ -52,7 +54,12 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
}
@Override
public GraphDisplay getGraphDisplay(boolean reuseGraph,
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) {
if (reuseGraph && !displays.isEmpty()) {
@ -62,7 +69,7 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
}
DefaultGraphDisplay display =
Swing.runNow(() -> new DefaultGraphDisplay(this, displayCounter++));
Swing.runNow(() -> new DefaultGraphDisplay(this, properties, displayCounter++));
displays.add(display);
return display;
}

View file

@ -19,7 +19,7 @@ import java.util.*;
import org.jungrapht.visualization.VisualizationServer;
import org.jungrapht.visualization.selection.MutableSelectedState;
import org.jungrapht.visualization.subLayout.VisualGraphCollapser;
import org.jungrapht.visualization.sublayout.VisualGraphCollapser;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
@ -31,26 +31,14 @@ import ghidra.service.graph.AttributedVertex;
public class GhidraGraphCollapser extends VisualGraphCollapser<AttributedVertex, AttributedEdge> {
public GhidraGraphCollapser(VisualizationServer<AttributedVertex, AttributedEdge> vv) {
super(vv, null);
}
@Override
public AttributedVertex collapse(Collection<AttributedVertex> selected) {
// Unusual Code Alert! - We are forced to set the vertex supplier here
// instead of in the constructor because we need the set of vertices that are
// going to be grouped at the GroupVertex construction time because it will create
// a final id that is based on its contained vertices. A better solution would
// be for the super class to take in a vertex factory that can take in the selected
// nodes as function parameter when creating the containing GroupVertex.
super.setVertexSupplier(() -> GroupVertex.groupVertices(selected));
return super.collapse(selected);
super(vv);
}
/**
* Ungroups any GroupVertices that are selected
*/
public void ungroupSelectedVertices() {
expand(vv.getSelectedVertexState().getSelected());
expand(vv.getSelectedVertices());
}
/**
@ -63,7 +51,7 @@ public class GhidraGraphCollapser extends VisualGraphCollapser<AttributedVertex,
MutableSelectedState<AttributedEdge> selectedEState = vv.getSelectedEdgeState();
Collection<AttributedVertex> selected = selectedVState.getSelected();
if (selected.size() > 1) {
AttributedVertex groupVertex = collapse(selected);
AttributedVertex groupVertex = collapse(selected, s -> GroupVertex.groupVertices(selected));
selectedVState.clear();
selectedEState.clear();
selectedVState.select(groupVertex);

View file

@ -27,6 +27,7 @@ import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import ghidra.service.graph.AttributedVertex;
import ghidra.service.graph.GraphDisplay;
public class GhidraIconCache {
@ -42,6 +43,8 @@ public class GhidraIconCache {
private final Map<AttributedVertex, Icon> map = new ConcurrentHashMap<>();
private final IconShape.Function iconShapeFunction = new IconShape.Function();
private String preferredLabel = null;
private int labelAlignment = GraphDisplay.ALIGN_CENTER;
Icon get(AttributedVertex vertex) {
@ -62,7 +65,7 @@ public class GhidraIconCache {
}
private Icon createIcon(AttributedVertex vertex) {
rendererLabel.setText(ProgramGraphFunctions.getLabel(vertex));
rendererLabel.setText(ProgramGraphFunctions.getLabel(vertex, preferredLabel));
rendererLabel.setFont(new Font(DEFAULT_FONT_NAME, Font.BOLD, DEFAULT_FONT_SIZE));
rendererLabel.setForeground(Color.black);
rendererLabel.setBackground(Color.white);
@ -193,4 +196,12 @@ public class GhidraIconCache {
public void evict(AttributedVertex vertex) {
map.remove(vertex);
}
public void setPreferredLabel(String attributeName) {
this.preferredLabel = attributeName;
}
public void setLabelAlignment(int labelAlignment) {
this.labelAlignment = labelAlignment;
}
}

View file

@ -106,8 +106,6 @@ class LayoutTransitionManager {
}
if (layoutAlgorithm instanceof TreeLayout) {
((TreeLayout<AttributedVertex>) layoutAlgorithm).setRootPredicate(rootPredicate);
layoutAlgorithm.setAfter(new PostProcessRunnable<>(
visualizationServer.getVisualizationModel().getLayoutModel()));
}
// remove any previously added layout paintables
removePaintable(radialLayoutRings);
@ -130,8 +128,7 @@ class LayoutTransitionManager {
((EdgeSorting<AttributedEdge>) layoutAlgorithm).setEdgeComparator(edgeComparator);
}
LayoutAlgorithmTransition.apply(visualizationServer,
layoutAlgorithm,
new PostProcessRunnable<>(visualizationServer.getVisualizationModel().getLayoutModel()));
layoutAlgorithm);
}
private void removePaintable(VisualizationServer.Paintable paintable) {
@ -150,9 +147,6 @@ class LayoutTransitionManager {
.setRootPredicate(rootPredicate);
((TreeLayout<AttributedVertex>) initialLayoutAlgorithm)
.setVertexBoundsFunction(vertexBoundsFunction);
initialLayoutAlgorithm.setAfter(new PostProcessRunnable<>(
visualizationServer.getVisualizationModel().getLayoutModel()));
}
if (initialLayoutAlgorithm instanceof EdgeSorting) {
((EdgeSorting<AttributedEdge>) initialLayoutAlgorithm)

View file

@ -1,80 +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.util.*;
import org.jgrapht.Graph;
import org.jungrapht.visualization.layout.model.LayoutModel;
/**
* to post-process tree layouts to move vertices that overlap a vertical edge that
* is not incident on the vertex.
* This can be removed after jungrapht-layout-1.1
* @param <V> vertex type
* @param <E> edge type
*/
public class PostProcessRunnable<V, E> implements Runnable {
LayoutModel<V> layoutModel;
public PostProcessRunnable(LayoutModel<V> layoutModel) {
this.layoutModel = layoutModel;
}
@Override
public void run() {
moveVerticesThatOverlapVerticalEdges(layoutModel);
}
protected int moveVerticesThatOverlapVerticalEdges(LayoutModel<V> layoutModel) {
int offset = 100;
int moved = 0;
Graph<V, E> graph = layoutModel.getGraph();
Map<Double, Set<E>> verticalEdgeMap = new LinkedHashMap<>();
graph.edgeSet()
.stream()
.filter(e -> layoutModel.apply(graph.getEdgeSource(e)).x == layoutModel
.apply(graph.getEdgeTarget(e)).x)
.forEach(e -> verticalEdgeMap
.computeIfAbsent(layoutModel.apply(graph.getEdgeSource(e)).x,
k -> new HashSet<>())
.add(e));
for (V v : graph.vertexSet()) {
double x = layoutModel.apply(v).x;
for (E edge : verticalEdgeMap.getOrDefault(x, Collections.emptySet())) {
V source = graph.getEdgeSource(edge);
V target = graph.getEdgeTarget(edge);
if (!v.equals(source) && !v.equals(target)) {
double lowy = layoutModel.apply(source).y;
double hiy = layoutModel.apply(target).y;
if (lowy > hiy) {
double temp = lowy;
lowy = hiy;
hiy = temp;
}
double vy = layoutModel.apply(v).y;
if (lowy <= vy && vy <= hiy) {
layoutModel.set(v, layoutModel.apply(v).add(offset, 0));
moved++;
}
}
}
}
return moved;
}
}

View file

@ -15,8 +15,6 @@
*/
package ghidra.graph.visualization;
import static org.jungrapht.visualization.VisualizationServer.*;
import java.awt.*;
import java.util.Map;
@ -28,6 +26,8 @@ import com.google.common.base.Splitter;
import ghidra.service.graph.Attributed;
import ghidra.service.graph.AttributedEdge;
import static org.jungrapht.visualization.layout.util.PropertyLoader.PREFIX;
/**
* a container for various functions used by ProgramGraph
*/
@ -125,13 +125,14 @@ abstract class ProgramGraphFunctions {
/**
* gets a display label from an {@link Attributed} object (vertex)
* @param attributed the attributed object to get a label for
* @param preferredLabelKey the attribute to use for the label, if available
* @return the label for the given {@link Attributed}
*/
public static String getLabel(Attributed attributed) {
public static String getLabel(Attributed attributed, String preferredLabelKey) {
Map<String, String> map = attributed.getAttributeMap();
String name = StringEscapeUtils.escapeHtml4(map.get("Name"));
if (map.containsKey("Code")) {
name = StringEscapeUtils.escapeHtml4(map.get("Code"));
if (map.containsKey(preferredLabelKey)) {
name = StringEscapeUtils.escapeHtml4(map.get(preferredLabelKey));
}
return "<html>" + String.join("<p>", Splitter.on('\n').split(name));
}

View file

@ -45,14 +45,6 @@ public abstract class AbstractJgtGraphMousePlugin<V, E>
protected V selectedVertex;
protected E selectedEdge;
public AbstractJgtGraphMousePlugin() {
this(InputEvent.BUTTON1_DOWN_MASK);
}
public AbstractJgtGraphMousePlugin(int selectionModifiers) {
super(selectionModifiers);
}
public VisualizationViewer<V, E> getViewer(MouseEvent e) {
VisualizationViewer<V, E> viewer = getGraphViewer(e);
return viewer;

View file

@ -33,10 +33,6 @@ import org.jungrapht.visualization.control.AbstractGraphMousePlugin;
public class JgtCursorRestoringPlugin<V, E> extends AbstractGraphMousePlugin
implements MouseMotionListener {
public JgtCursorRestoringPlugin() {
super(0);
}
@Override
public void mouseDragged(MouseEvent e) {
// don't care

View file

@ -34,10 +34,19 @@ import ghidra.graph.visualization.CenterAnimationJob;
*/
public class JgtEdgeNavigationPlugin<V, E> extends AbstractJgtGraphMousePlugin<V, E> {
public JgtEdgeNavigationPlugin() {
protected int getSingleSelectionMask;
public JgtEdgeNavigationPlugin(int singleSelectionMask) {
this.singleSelectionMask = singleSelectionMask;
this.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
protected int singleSelectionMask;
public boolean checkModifiers(MouseEvent e) {
return e.getModifiersEx() == singleSelectionMask;
}
@Override
public void mousePressed(MouseEvent e) {
if (!checkModifiers(e)) {

View file

@ -15,15 +15,14 @@
*/
package ghidra.graph.visualization.mouse;
import java.awt.event.InputEvent;
import org.jungrapht.visualization.control.*;
import docking.DockingUtils;
import ghidra.graph.visualization.DefaultGraphDisplay;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
import java.awt.event.InputEvent;
/**
* Pluggable graph mouse for jungrapht
*/
@ -34,7 +33,7 @@ public class JgtPluggableGraphMouse extends DefaultGraphMouse<AttributedVertex,
// TODO we should not need the graph display for any mouse plugins, but the API is net yet
// robust enough to communicate fully without it
public JgtPluggableGraphMouse(DefaultGraphDisplay graphDisplay) {
super(DefaultGraphMouse.<AttributedVertex, AttributedEdge> builder());
super(DefaultGraphMouse.builder());
this.graphDisplay = graphDisplay;
}
@ -47,23 +46,22 @@ public class JgtPluggableGraphMouse extends DefaultGraphMouse<AttributedVertex,
//
// edge
add(new JgtEdgeNavigationPlugin<AttributedVertex, AttributedEdge>());
add(new JgtEdgeNavigationPlugin<>(InputEvent.BUTTON1_DOWN_MASK));
add(new JgtVertexFocusingPlugin<AttributedVertex, AttributedEdge>(graphDisplay));
add(new JgtVertexFocusingPlugin<>(InputEvent.BUTTON1_DOWN_MASK, graphDisplay));
// scaling
add(new ScalingGraphMousePlugin(new CrossoverScalingControl(), 0, in, out));
add(new SelectingGraphMousePlugin<>());
add(new RegionSelectingGraphMousePlugin<>());
// the grab/pan feature
add(new JgtTranslatingPlugin<AttributedVertex, AttributedEdge>());
add(new TranslatingGraphMousePlugin(InputEvent.BUTTON1_DOWN_MASK));
add(new SelectingGraphMousePlugin<AttributedVertex, AttributedEdge>(
InputEvent.BUTTON1_DOWN_MASK,
0,
DockingUtils.CONTROL_KEY_MODIFIER_MASK));
// scaling
add(new ScalingGraphMousePlugin());
// cursor cleanup
add(new JgtCursorRestoringPlugin<AttributedVertex, AttributedEdge>());
add(new JgtCursorRestoringPlugin<>());
setPluginsLoaded();
}

View file

@ -40,16 +40,21 @@ public class JgtTranslatingPlugin<V, E>
private boolean panning;
private boolean isHandlingEvent;
private int translatingMask;
public JgtTranslatingPlugin() {
this(InputEvent.BUTTON1_DOWN_MASK);
}
public JgtTranslatingPlugin(int modifiers) {
super(modifiers);
this.translatingMask = modifiers;
this.cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
public boolean checkModifiers(MouseEvent e) {
return e.getModifiersEx() == translatingMask;
}
@Override
public void mousePressed(MouseEvent e) {
boolean accepted = checkModifiers(e) && isInDraggingArea(e);

View file

@ -15,8 +15,6 @@
*/
package ghidra.graph.visualization.mouse;
import static org.jungrapht.visualization.VisualizationServer.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
@ -27,6 +25,8 @@ import org.jungrapht.visualization.control.TransformSupport;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.selection.ShapePickSupport;
import static org.jungrapht.visualization.layout.util.PropertyLoader.PREFIX;
/**
* Keeper of shared logic for jungrapht handling
*/

View file

@ -29,11 +29,18 @@ import ghidra.service.graph.AttributedVertex;
public class JgtVertexFocusingPlugin<V, E> extends AbstractJgtGraphMousePlugin<V, E> {
private DefaultGraphDisplay graphDisplay;
protected int singleSelectionMask;
public JgtVertexFocusingPlugin(DefaultGraphDisplay graphDisplay) {
public JgtVertexFocusingPlugin(int singleSelectionMask, DefaultGraphDisplay graphDisplay) {
this.singleSelectionMask = singleSelectionMask;
this.graphDisplay = graphDisplay;
}
@Override
public boolean checkModifiers(MouseEvent e) {
return e.getModifiersEx() == singleSelectionMask;
}
@Override
public void mousePressed(MouseEvent e) {
if (!checkModifiers(e)) {

View file

@ -46,9 +46,6 @@ pass
# how many times to iterate over the layers while swapping node positions (mincross)
jungrapht.mincrossTransposeLimit=5
# not using spatial data structures for edges
jungrapht.edgeSpatialSupport=NONE
# over 200 and the eiglsperger algorithm will run instead of the sugiyama
jungrapht.mincross.eiglspergerThreshold=200
@ -63,5 +60,21 @@ jungrapht.initialDimensionVertexDensity=0.3f
jungrapht.minScale=0.001
jungrapht.maxScale=4.0
# not using spatial data structures for vertices at this time. May remove after jungrapht 1.1
jungrapht.vertexSpatialSupport=NONE
# spatial data structures for vertices
jungrapht.vertexSpatialSupport=RTREE
# spatial data structures for edges
jungrapht.edgeSpatialSupport=RTREE
#mouse modifiers
# the mask for single vertex/edge selection
jungrapht.singleSelectionMask=MB1
# the mask to augment the selection with a single vertex/edge
jungrapht.addSingleSelectionMask=MB1_MENU
# the mask to select vertices within a region
jungrapht.regionSelectionMask=MB1_MENU
# the mask to augment the selection with vertices in a region
jungrapht.addRegionSelectionMask=MB1_SHIFT_MENU
# the mask to indicate that the selection region is complete/closed and selection may commence
jungrapht.regionSelectionCompleteMask=MENU
# the mask to indicate that the selection region for augmentation is complete/closed and selection may commence
jungrapht.addRegionSelectionCompleteMask=SHIFT_MENU

View file

@ -15,6 +15,8 @@
*/
package ghidra.service.graph;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import docking.action.DockingAction;
@ -33,6 +35,8 @@ public interface GraphDisplay {
public static final int ALIGN_LEFT = 0; // aligns graph text to the left
public static final int ALIGN_CENTER = 1; // aligns graph text to the center
public static final int ALIGN_RIGHT = 2; // aligns graph text to the right
public static final int ALIGN_TOP = 3; // aligns graph text to the right
public static final int ALIGN_BOTTOM = 4; // aligns graph text to the right
/**
* Sets a {@link GraphDisplayListener} to be notified when the user changes the vertex focus
@ -108,7 +112,7 @@ public interface GraphDisplay {
/**
* Sets the name of the attribute which should be used as the primary vertex label in the display.
* @param attributeName the name of the attribute to use as the display label for vertices.
* @param alignment (ALIGN_LEFT, ALIGN_RIGHT, or ALIGN_CENTER)
* @param alignment (ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER, ALIGN_TOP, or ALIGN_BOTTOM)
* @param size the font size to use for the display label
* @param monospace true if the font should be monospaced
* @param maxLines the maximum number lines to display in the vertex labels
@ -153,4 +157,15 @@ public interface GraphDisplay {
*/
public void addAction(DockingAction action);
default void setProperty(String key, String value) {
}
default String getValue(String key) {
return null;
}
default Map<String, String> getProperties() {
return Collections.emptyMap();
}
}

View file

@ -22,6 +22,9 @@ import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.exception.GraphException;
import ghidra.util.task.TaskMonitor;
import java.util.Collections;
import java.util.Map;
/**
* Basic interface for objects that can display or otherwise consume a generic graph
*/
@ -41,8 +44,12 @@ public interface GraphDisplayProvider extends ExtensionPoint {
* @return A GraphDisplay that can be used to display (or otherwise consume - e.g. export) the graph
* @throws GraphException thrown if there is a problem creating a GraphDisplay
*/
public GraphDisplay getGraphDisplay(boolean reuseGraph,
TaskMonitor monitor) throws GraphException;
public GraphDisplay getGraphDisplay(boolean reuseGraph, TaskMonitor monitor) throws GraphException;
default GraphDisplay getGraphDisplay(boolean reuseGraph, Map<String, String> properties,
TaskMonitor monitor) throws GraphException {
return getGraphDisplay(reuseGraph, monitor);
}
/**
* Provides an opportunity for this provider to register and read tool options

View file

@ -10,3 +10,5 @@ NOTICE||GHIDRA||||END|
README.md||GHIDRA||||END|
build.gradle||GHIDRA||||END|
settings.gradle||GHIDRA||||END|
tldr.bat||GHIDRA||||END|
tldr.sh||GHIDRA||||END|