GP-1804 - Added an API method to the GraphDisplayProvider to get the active graph

This commit is contained in:
dragonmacher 2022-03-15 16:19:44 -04:00
parent a7e9073798
commit 907c084b25
8 changed files with 169 additions and 116 deletions

View File

@ -42,7 +42,7 @@ public interface GraphDisplayBroker {
/**
* Adds a listener for notification when the set of graph display providers change or the currently
* active graph display provider changes
* active graph display provider changes
* @param listener the listener to be notified
*/
public void addGraphDisplayBrokerListener(GraphDisplayBrokerListener listener);
@ -54,7 +54,9 @@ public interface GraphDisplayBroker {
public void removeGraphDisplayBrokerLisetener(GraphDisplayBrokerListener listener);
/**
* A convenience method for getting a {@link GraphDisplay} from the currently active provider
* A convenience method for getting a {@link GraphDisplay} from the currently active provider.
* This method is intended to be used to display a new graph.
*
* @param reuseGraph if true, the provider will attempt to re-use a current graph display
* @param monitor the {@link TaskMonitor} that can be used to cancel the operation
* @return a {@link GraphDisplay} object to sends graphs to be displayed or exported.

View File

@ -15,6 +15,9 @@
*/
package ghidra.graph.export;
import java.util.Collections;
import java.util.List;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.service.graph.GraphDisplay;
@ -28,7 +31,7 @@ import ghidra.util.task.TaskMonitor;
* {@link GraphDisplay} is mostly just a placeholder for executing the export function. By
* hijacking the {@link GraphDisplayProvider} and {@link GraphDisplay} interfaces for exporting,
* all graph generating operations can be exported instead of being displayed without changing
* the graph generation code.
* the graph generation code.
*/
public class ExportAttributedGraphDisplayProvider implements GraphDisplayProvider {
@ -51,10 +54,19 @@ public class ExportAttributedGraphDisplayProvider implements GraphDisplayProvide
@Override
public GraphDisplay getGraphDisplay(boolean reuseGraph,
TaskMonitor monitor) {
return new ExportAttributedGraphDisplay(this);
}
@Override
public GraphDisplay getActiveGraphDisplay() {
return null; // one-time graph; no active graph
}
@Override
public List<GraphDisplay> getAllGraphDisplays() {
return Collections.emptyList(); // one-time graph; no active displays
}
@Override
public void initialize(PluginTool tool, Options graphOptions) {
this.pluginTool = tool;

View File

@ -37,7 +37,6 @@ import org.jungrapht.visualization.layout.algorithms.LayoutAlgorithm;
import org.jungrapht.visualization.layout.algorithms.util.InitialDimensionFunction;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.model.Point;
import org.jungrapht.visualization.renderers.Renderer.VertexLabel.Position;
import org.jungrapht.visualization.selection.MutableSelectedState;
import org.jungrapht.visualization.transform.*;
import org.jungrapht.visualization.transform.shape.MagnifyImageLensSupport;
@ -71,24 +70,6 @@ import resources.Icons;
/**
* Delegates to a {@link VisualizationViewer} to draw a graph visualization
*
* <P>This graph uses the following properties:
* <UL>
* <LI>selectedVertexColor - hex color using '0x' or '#', with 6 digits
* </LI>
* <LI>selectedEdgeColor - hex color using '0x' or '#', with 6 digits
* </LI>
* <LI>displayVerticesAsIcons - if true, shapes will be used to draw vertices based upon
* {@link GhidraIconCache}; false, then vertex shapes will be created from
* {@link ProgramGraphFunctions#getVertexShape(Attributed)}
* </LI>
* <LI>vertexLabelPosition - see {@link Position}
* </LI>
* <LI>initialLayoutAlgorithm - the name of the layout algorithm to be used for the initial
* graph layout
* </LI>
* </UL>
*
*/
public class DefaultGraphDisplay implements GraphDisplay {
@ -125,9 +106,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
private final DefaultGraphDisplayComponentProvider componentProvider;
/**
* Whether to ensure the focused vertex is visible, scrolling if necessary
* the visualization in order to center the selected vertex
* or the center of the set of selected vertices
* Whether to ensure the focused vertex is visible, scrolling if necessary the visualization in
* order to center the selected vertex or the center of the set of selected vertices
*/
private boolean ensureVertexIsVisible = false;
@ -150,9 +130,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
*/
private final GraphJobRunner jobRunner = new GraphJobRunner();
/**
* a satellite view that shows in the lower left corner as a birds-eye view of the graph display
*/
private final SatelliteVisualizationViewer<AttributedVertex, AttributedEdge> satelliteViewer;
private FilterDialog filterDialog;
@ -299,9 +276,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
.build();
}
/**
* create the highlighters ({@code Paintable}s to show which vertices have been selected or focused)
*/
private void buildHighlighers() {
viewer.removePostRenderPaintable(multiSelectedVertexPaintable);
@ -358,10 +332,10 @@ public class DefaultGraphDisplay implements GraphDisplay {
((AbstractButton) context.getSourceObject()).isSelected())
.buildAndInstallLocal(componentProvider);
this.ensureVertexIsVisible = true; // since we intialized action to selected
this.ensureVertexIsVisible = true; // since we initialized action to selected
// create a toggle for enabling 'free-form' selection: selection is
// inside of a traced shape instead of a rectangle
// create a toggle for enabling 'free-form' selection: selection is inside of a traced
// shape instead of a rectangle
new ToggleActionBuilder("Free-Form Selection", ACTION_OWNER)
.toolBarIcon(DefaultDisplayGraphIcons.LASSO_ICON)
.description("Trace Free-Form Shape to select multiple vertices (CTRL-click-drag)")
@ -594,9 +568,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
}
/**
* Group the selected vertices into one vertex that represents them all
*/
private void groupSelectedVertices() {
AttributedVertex vertex = graphCollapser.groupSelectedVertices();
if (vertex != null) {
@ -615,9 +586,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
/**
* Ungroup the selected vertices. If the focusedVertex is no longer
* in the graph, null it. This will happen if the focusedVertex was
* the GroupVertex
* Ungroup the selected vertices. If the focusedVertex is no longer in the graph, null it. This
* will happen if the focusedVertex was the GroupVertex
*/
private void ungroupSelectedVertices() {
graphCollapser.ungroupSelectedVertices();
@ -678,18 +648,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
// select all the edges that connect the supplied vertices
private void selectEdgesConnecting(Collection<AttributedVertex> vertices) {
viewer.getSelectedEdgeState()
.select(
graph.edgeSet()
.stream()
.filter(
e -> {
AttributedVertex source = graph.getEdgeSource(e);
AttributedVertex target = graph.getEdgeTarget(e);
return vertices.contains(source) && vertices.contains(target);
})
.collect(Collectors.toSet()));
Set<AttributedEdge> edges = graph.edgeSet()
.stream()
.filter(
e -> {
AttributedVertex source = graph.getEdgeSource(e);
AttributedVertex target = graph.getEdgeTarget(e);
return vertices.contains(source) && vertices.contains(target);
})
.collect(Collectors.toSet());
viewer.getSelectedEdgeState().select(edges);
}
private boolean isAllSelected(Set<AttributedVertex> vertices) {
@ -919,9 +887,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
viewer.getSelectedVertexState().select(Set.of(source, target));
}
/**
* connect the selection state to to the visualization
*/
private void connectSelectionStateListeners() {
switchableSelectionListener = new SwitchableSelectionItemListener();
viewer.getSelectedVertexState().addItemListener(switchableSelectionListener);
@ -1180,7 +1145,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
/**
* cause the graph to be centered and scaled nicely for the view window
* Cause the graph to be centered and scaled nicely for the view window
*/
public void centerAndScale() {
viewer.scaleToLayout();
@ -1301,7 +1266,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
});
// We control tooltips with the PopupRegulator. Use null values to disable the default
// We control tooltips with the PopupRegulator. Use null values to disable the default
// tool tip mechanism
vv.setVertexToolTipFunction(v -> null);
vv.setEdgeToolTipFunction(e -> null);
@ -1452,9 +1417,14 @@ public class DefaultGraphDisplay implements GraphDisplay {
});
}
@Override
public String toString() {
return getClass().getSimpleName() + " " + displayId;
}
//==================================================================================================
// Inner Classes
//==================================================================================================
//==================================================================================================
// class passed to the PopupRegulator to help construct info popups for the graph
private class GraphDisplayPopupSource implements PopupSource<AttributedVertex, AttributedEdge> {
@ -1468,8 +1438,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public ToolTipInfo<?> getToolTipInfo(MouseEvent event) {
// check for a vertex hit first, otherwise, we get edge hits when we are hovering
// over a vertex, due to how edges are interpreted as existing all the way to the
// check for a vertex hit first, otherwise, we get edge hits when we are hovering
// over a vertex, due to how edges are interpreted as existing all the way to the
// center point of a vertex
AttributedVertex vertex = getVertex(event);
if (vertex != null) {
@ -1523,8 +1493,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
/**
* Item listener for selection changes in the graph with the additional
* capability of being able to disable the listener without removing it.
* Item listener for selection changes in the graph with the additional
* capability of being able to disable the listener without removing it.
*/
private class SwitchableSelectionItemListener implements ItemListener {
boolean enabled = true;
@ -1537,9 +1507,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
private void run(ItemEvent e) {
// there was a change in the set of selected vertices.
// if the focused vertex is null, set it from one of the selected
// vertices
// There was a change in the set of selected vertices. If the focused vertex is null,
// set it from one of the selected vertices
if (e.getStateChange() == ItemEvent.SELECTED) {
Set<AttributedVertex> selectedVertices = getSelectedVertices();
notifySelectionChanged(new HashSet<>(selectedVertices));

View File

@ -15,8 +15,8 @@
*/
package ghidra.graph.visualization;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.framework.options.Options;
import ghidra.framework.options.PreferenceState;
@ -55,7 +55,7 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
public GraphDisplay getGraphDisplay(boolean reuseGraph, TaskMonitor monitor) {
if (reuseGraph && !displays.isEmpty()) {
DefaultGraphDisplay visibleGraph = getVisibleGraph();
DefaultGraphDisplay visibleGraph = (DefaultGraphDisplay) getActiveGraphDisplay();
visibleGraph.restoreToDefaultSetOfActions();
return visibleGraph;
}
@ -66,6 +66,22 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
return display;
}
@Override
public GraphDisplay getActiveGraphDisplay() {
if (displays.isEmpty()) {
return null;
}
return getAllGraphDisplays().get(0);
}
@Override
public List<GraphDisplay> getAllGraphDisplays() {
return displays.stream()
.filter(d -> d.getComponent().isShowing())
.sorted((d1, d2) -> -(d1.getId() - d2.getId())) // largest/newest IDs come first
.collect(Collectors.toList());
}
@Override
public void initialize(PluginTool tool, Options graphOptions) {
this.pluginTool = tool;
@ -78,20 +94,6 @@ public class DefaultGraphDisplayProvider implements GraphDisplayProvider {
defaultSatelliteState = preferences.getBoolean(DEFAULT_SATELLITE_STATE, false);
}
/**
* Get a {@code GraphDisplay} that is 'showing', assuming that is the one the user
* wishes to append to.
* Called only when displays is not empty. If there are no 'showing' displays,
* return one from the Set via its iterator
* @return a display that is showing
*/
private DefaultGraphDisplay getVisibleGraph() {
return displays.stream()
.filter(d -> d.getComponent().isShowing())
.findAny()
.orElse(displays.iterator().next());
}
@Override
public void optionsChanged(Options graphOptions) {
// no supported options

View File

@ -26,7 +26,7 @@ import ghidra.util.task.TaskMonitor;
* Interface for objects that display (or consume) graphs. Normally, a graph display represents
* a visual component for displaying and interacting with a graph. Some implementation may not
* be a visual component, but instead consumes/processes the graph (i.e. graph exporter). In this
* case, there is no interactive element and once the graph has been set on the display, it is
* case, there is no interactive element and once the graph has been set on the display, it is
* closed.
*/
public interface GraphDisplay {
@ -37,11 +37,13 @@ public interface GraphDisplay {
/**
* values are color names or rgb in hex '0xFF0000' is red
*/
String SELECTED_VERTEX_COLOR = "selectedVertexColor";
public static final String SELECTED_VERTEX_COLOR = "selectedVertexColor";
/**
* values are color names or rgb in hex '0xFF0000' is red
*/
String SELECTED_EDGE_COLOR = "selectedEdgeColor";
public static final String SELECTED_EDGE_COLOR = "selectedEdgeColor";
/**
* values are defined as String symbols in LayoutFunction class
*
@ -52,33 +54,38 @@ public interface GraphDisplay {
*
* may have no meaning for a different graph visualization library
*/
String INITIAL_LAYOUT_ALGORITHM = "initialLayoutAlgorithm";
public static final String INITIAL_LAYOUT_ALGORITHM = "initialLayoutAlgorithm";
/**
* true or false
* may have no meaning for a different graph visualization library
*/
String DISPLAY_VERTICES_AS_ICONS = "displayVerticesAsIcons";
public static final String DISPLAY_VERTICES_AS_ICONS = "displayVerticesAsIcons";
/**
* values are the strings N,NE,E,SE,S,SW,W,NW,AUTO,CNTR
* may have no meaning for a different graph visualization library
*/
String VERTEX_LABEL_POSITION = "vertexLabelPosition";
public static final String VERTEX_LABEL_POSITION = "vertexLabelPosition";
/**
* true or false, whether edge selection via a mouse click is enabled.
* May not be supported by another graph visualization library
*/
String ENABLE_EDGE_SELECTION = "enableEdgeSelection";
public static final String ENABLE_EDGE_SELECTION = "enableEdgeSelection";
/**
* a comma-separated list of edge type names in priority order
*/
String EDGE_TYPE_PRIORITY_LIST = "edgeTypePriorityList";
public static final String EDGE_TYPE_PRIORITY_LIST = "edgeTypePriorityList";
/**
* a comma-separated list of edge type names.
* any will be considered a favored edge for the min-cross layout
* algorithms.
* May have no meaning with a different graph visualization library
*/
String FAVORED_EDGES = "favoredEdges";
public static final String FAVORED_EDGES = "favoredEdges";
/**
* Sets a {@link GraphDisplayListener} to be notified when the user changes the vertex focus
@ -121,7 +128,7 @@ public interface GraphDisplay {
* @param eventTrigger Provides a hint to the GraphDisplay as to why we are updating the
* graph location so that the GraphDisplay can decide if it should send out a notification via
* the {@link GraphDisplayListener#selectionChanged(Set)}. For example, if we are updating
* the the location due to an event from the main application, we don't want to notify the
* the the location due to an event from the main application, we don't want to notify the
* application the graph changed to avoid event cycles. See {@link EventTrigger} for more
* information.
*/
@ -153,6 +160,7 @@ public interface GraphDisplay {
TaskMonitor monitor) throws CancelledException {
setGraph(graph, new GraphDisplayOptions(graph.getGraphType()), title, append, monitor);
}
/**
* Sets the graph to be displayed or consumed by this graph display
*

View File

@ -15,6 +15,8 @@
*/
package ghidra.service.graph;
import java.util.List;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
@ -38,11 +40,28 @@ public interface GraphDisplayProvider extends ExtensionPoint {
*
* @param reuseGraph if true, this provider will attempt to re-use an existing GraphDisplay
* @param monitor the {@link TaskMonitor} that can be used to monitor and cancel the operation
* @return A GraphDisplay that can be used to display (or otherwise consume - e.g. export) the graph
* @return an object 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;
/**
* Returns the active graph display or null if there is no active graph display. If only one
* graph is displayed, then that graph will be returned. If multiple graphs are being
* displayed, then the most recently shown graph will be displayed, regardless of whether that
* is the active graph in terms of user interaction.
*
* @return the active graph display or null if there is no active graph display.
*/
public GraphDisplay getActiveGraphDisplay();
/**
* Returns all known graph displays. Typically they will be ordered by use, most recently
* first.
* @return the displays
*/
public List<GraphDisplay> getAllGraphDisplays();
/**
* Provides an opportunity for this provider to register and read tool options

View File

@ -91,6 +91,11 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
}
private void close(GraphDisplay gd) {
runSwing(() -> gd.close());
waitForSwing();
}
@Test
public void testDeSelectVertexAction() {
select(a, b, c, d);
@ -409,8 +414,35 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
assertTrue(graphSpy.isSelected(a, b, c, d));
}
private void clearSelection() {
select();
@Test
public void testGetActiveGraph() throws Exception {
GraphDisplayBroker broker = tool.getService(GraphDisplayBroker.class);
GraphDisplayProvider service = broker.getGraphDisplayProvider("Default Graph Display");
GraphDisplay firstDisplay = service.getActiveGraphDisplay();
assertNotNull(firstDisplay);
showGraph();
GraphDisplay secondDisplay = service.getActiveGraphDisplay();
assertNotNull(secondDisplay);
assertNotSame(firstDisplay, secondDisplay);
showGraph();
GraphDisplay thirdDisplay = service.getActiveGraphDisplay();
assertNotNull(thirdDisplay);
assertNotSame(firstDisplay, thirdDisplay);
assertNotSame(secondDisplay, thirdDisplay);
close(thirdDisplay);
close(firstDisplay);
GraphDisplay activeDisplay = service.getActiveGraphDisplay();
assertNotNull(activeDisplay);
assertSame(secondDisplay, activeDisplay);
close(secondDisplay);
activeDisplay = service.getActiveGraphDisplay();
assertNull(activeDisplay);
}
private void collapse() {
@ -422,6 +454,10 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
pressButtonByText(dialog, "OK", true);
}
private void clearSelection() {
select();
}
private void expand() {
DockingActionIf action = getAction(tool, "Expand Selected");
GraphActionContext context =
@ -453,12 +489,13 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
try {
display.setGraph(graph, options, "test graph", false, TaskMonitor.DUMMY);
}
catch (CancelledException e) {
catch (CancelledException ce) {
// can't happen with a dummy monitor
}
});
display.setGraphDisplayListener(new TestGraphDisplayListener("test"));
display.setGraphDisplayListener(new TestGraphDisplayListener());
waitForSwing();
}
private void select(AttributedVertex... vertices) {
@ -484,13 +521,7 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
runSwing(() -> display.setFocusedVertex(vertex, trigger));
}
class TestGraphDisplayListener implements GraphDisplayListener {
private String name;
TestGraphDisplayListener(String name) {
this.name = name;
}
private class TestGraphDisplayListener implements GraphDisplayListener {
@Override
public void graphClosed() {
@ -509,7 +540,7 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
@Override
public GraphDisplayListener cloneWith(GraphDisplay graphDisplay) {
return new TestGraphDisplayListener("clone");
return new TestGraphDisplayListener();
}
@Override
@ -519,7 +550,7 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
}
class GraphSpy {
private class GraphSpy {
AttributedVertex focusedVertex;
Set<AttributedVertex> selectedVertices;
@ -532,8 +563,8 @@ public class GraphActionsTest extends AbstractGhidraHeadedIntegrationTest {
return expected.equals(selectedVertices);
}
public boolean isFocused(AttributedVertex a) {
return a == focusedVertex;
public boolean isFocused(AttributedVertex v) {
return v == focusedVertex;
}
public void clear() {

View File

@ -15,6 +15,9 @@
*/
package ghidra.graph;
import java.util.Collections;
import java.util.List;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.service.graph.GraphDisplay;
@ -38,25 +41,32 @@ public class TestGraphService implements GraphDisplayProvider {
}
@Override
public void initialize(PluginTool tool, Options options) {
// nothing
public GraphDisplay getActiveGraphDisplay() {
return null;
}
@Override
public List<GraphDisplay> getAllGraphDisplays() {
return Collections.emptyList();
}
@Override
public void initialize(PluginTool tool, Options options) {
// stub
}
@Override
public void optionsChanged(Options options) {
// nothing
// stub
}
@Override
public void dispose() {
// nothing
// stub
}
@Override
public HelpLocation getHelpLocation() {
return null;
}
}