Merge remote-tracking branch

'origin/GP_371_ghidravore_vertex_collapse--SQUASHED'

Conflicts:
	Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java
This commit is contained in:
ghidravore 2020-11-20 16:52:32 -05:00
commit e6fe576b87
8 changed files with 553 additions and 51 deletions

View file

@ -206,6 +206,13 @@
<LI><A name="Display_Popup_Windows">
<B>Display Popup Windows</B> - When toggled off no tooltip popups will be displayed.</LI>
<LI><A name="Collapse_Selected">
<B>Collapse Selected Vertices</B> - The selected vertices are grouped into a single vertex.</LI>
<LI><A name="Expand_Selected">
<B>Expand Selected Vertices</B> - Any group vertices are reverted back to the vertices that it contains.</LI>
</UL>

View file

@ -161,6 +161,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
private ToggleDockingAction togglePopupsAction;
private PopupRegulator<AttributedVertex, AttributedEdge> popupRegulator;
private GhidraGraphCollapser graphCollapser;
/**
* Create the initial display, the graph-less visualization viewer, and its controls
@ -439,6 +440,20 @@ public class DefaultGraphDisplay implements GraphDisplay {
.onAction(c -> createAndDisplaySubGraph())
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Collapse Selected", ACTION_OWNER)
.popupMenuPath("Collapse Selected Vertices")
.popupMenuGroup("zz", "6")
.description("Collapses the selected vertices into one collapsed vertex")
.onAction(c -> groupSelectedVertices())
.buildAndInstallLocal(componentProvider);
new ActionBuilder("Expand Selected", ACTION_OWNER)
.popupMenuPath("Expand Selected Vertices")
.popupMenuGroup("zz", "6")
.description("Expands all selected collapsed vertices into their previous form")
.onAction(c -> graphCollapser.ungroupSelectedVertices())
.buildAndInstallLocal(componentProvider);
togglePopupsAction = new ToggleActionBuilder("Display Popup Windows", ACTION_OWNER)
.popupMenuPath("Display Popup Windows")
.popupMenuGroup("zz", "1")
@ -450,6 +465,17 @@ 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) {
focusedVertex = vertex;
scrollToSelected(vertex);
}
}
private void clearSelection() {
viewer.getSelectedVertexState().clear();
viewer.getSelectedEdgeState().clear();
@ -675,7 +701,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public void setFocusedVertex(AttributedVertex vertex, EventTrigger eventTrigger) {
boolean changed = this.focusedVertex != vertex;
this.focusedVertex = vertex;
this.focusedVertex = graphCollapser.getOutermostVertex(vertex);
if (focusedVertex != null) {
if (changed && eventTrigger != EventTrigger.INTERNAL_ONLY) {
notifyLocationFocusChanged(focusedVertex);
@ -704,23 +730,27 @@ public class DefaultGraphDisplay implements GraphDisplay {
return true;
}
@SuppressWarnings("unchecked")
private Collection<AttributedVertex> getVertices(Object item) {
if (item instanceof Collection) {
return (Collection<AttributedVertex>) item;
}
else if (item instanceof AttributedVertex) {
return List.of((AttributedVertex) item);
}
return Collections.emptyList();
}
/**
* fire an event to notify the selected vertices changed
* @param selected the list of selected vertices
*/
private void notifySelectionChanged(Set<AttributedVertex> selected) {
Swing.runLater(() -> listener.selectionChanged(selected));
// replace any group vertices with their individual vertices.
Set<AttributedVertex> flattened = GroupVertex.flatten(selected);
Swing.runLater(() -> listener.selectionChanged(flattened));
}
public static Set<AttributedVertex> flatten(Collection<AttributedVertex> vertices) {
Set<AttributedVertex> set = new HashSet<>();
for (AttributedVertex vertex : vertices) {
if (vertex instanceof GroupVertex) {
set.addAll(((GroupVertex) vertex).getContainedVertices());
}
else {
set.add(vertex);
}
}
return set;
}
/**
@ -728,25 +758,26 @@ public class DefaultGraphDisplay implements GraphDisplay {
* @param vertex the new focused vertex
*/
private void notifyLocationFocusChanged(AttributedVertex vertex) {
Swing.runLater(() -> listener.locationFocusChanged(vertex));
AttributedVertex focus =
vertex instanceof GroupVertex ? ((GroupVertex) vertex).getFirst() : vertex;
Swing.runLater(() -> listener.locationFocusChanged(focus));
}
@Override
public void selectVertices(Set<AttributedVertex> selected, EventTrigger eventTrigger) {
// if we are not to fire events, turn off the selection listener we provided to the
// graphing library.
switchableSelectionListener.setEnabled(eventTrigger != EventTrigger.INTERNAL_ONLY);
boolean fireEvents = eventTrigger != EventTrigger.INTERNAL_ONLY;
switchableSelectionListener.setEnabled(fireEvents);
try {
Set<AttributedVertex> vertices = graphCollapser.convertToOutermostVertices(selected);
MutableSelectedState<AttributedVertex> nodeSelectedState =
viewer.getSelectedVertexState();
if (selected.isEmpty()) {
nodeSelectedState.clear();
}
else if (!Arrays.asList(nodeSelectedState.getSelectedObjects()).containsAll(selected)) {
nodeSelectedState.clear();
nodeSelectedState.select(selected, false);
scrollToSelected(selected);
nodeSelectedState.clear();
if (!vertices.isEmpty()) {
nodeSelectedState.select(vertices, fireEvents);
scrollToSelected(vertices);
}
viewer.repaint();
}
@ -901,6 +932,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
graph.addVertex("1", "Graph Aborted");
}
doSetGraphData(graph);
graphCollapser = new GhidraGraphCollapser(viewer);
}
private AttributedGraph mergeGraphs(AttributedGraph newGraph, AttributedGraph oldGraph) {
@ -994,7 +1027,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
@Override
public void updateVertexName(AttributedVertex vertex, String newName) {
vertex.setName(newName);
vertex.clearCache();
iconCache.evict(vertex);
viewer.repaint();
}
@ -1143,8 +1175,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
// if the focused vertex is null, set it from one of the selected
// vertices
if (e.getStateChange() == ItemEvent.SELECTED) {
Collection<AttributedVertex> selectedVertices = getVertices(e.getItem());
notifySelectionChanged(new HashSet<>(selectedVertices));
Set<AttributedVertex> selectedVertices = getSelectedVertices();
notifySelectionChanged(new HashSet<AttributedVertex>(selectedVertices));
if (selectedVertices.size() == 1) {
// if only one vertex was selected, make it the focused vertex
@ -1157,7 +1189,8 @@ public class DefaultGraphDisplay implements GraphDisplay {
}
}
else if (e.getStateChange() == ItemEvent.DESELECTED) {
notifySelectionChanged(Collections.emptySet());
Set<AttributedVertex> selectedVertices = getSelectedVertices();
notifySelectionChanged(selectedVertices);
}
viewer.repaint();
}
@ -1384,4 +1417,5 @@ public class DefaultGraphDisplay implements GraphDisplay {
// this graph display does not have a notion of emphasizing
}
}
}

View file

@ -0,0 +1,109 @@
/* ###
* 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.jungrapht.visualization.VisualizationServer;
import org.jungrapht.visualization.selection.MutableSelectedState;
import org.jungrapht.visualization.subLayout.VisualGraphCollapser;
import ghidra.service.graph.AttributedEdge;
import ghidra.service.graph.AttributedVertex;
/**
* Handles collapsing graph nodes. Had to subclass because the GroupVertex supplier needed
* access to the items it contain at creation time.
*/
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);
}
/**
* Ungroups any GroupVertices that are selected
*/
public void ungroupSelectedVertices() {
expand(vv.getSelectedVertexState().getSelected());
}
/**
* Group the selected vertices into one vertex that represents them all
*
* @return the new GroupVertex
*/
public AttributedVertex groupSelectedVertices() {
MutableSelectedState<AttributedVertex> selectedVState = vv.getSelectedVertexState();
MutableSelectedState<AttributedEdge> selectedEState = vv.getSelectedEdgeState();
Collection<AttributedVertex> selected = selectedVState.getSelected();
if (selected.size() > 1) {
AttributedVertex groupVertex = collapse(selected);
selectedVState.clear();
selectedEState.clear();
selectedVState.select(groupVertex);
return groupVertex;
}
return null;
}
/**
* Converts the given set of vertices to a new set where any vertices that are part of a group
* are replaced with the outermost group containing it.
*
* @param vertices the set of vertices to possibly convert to containing group nodes
* @return a converted set of vertices where all vertices part of a group have been replace with
* its containing outermost GroupNode.
*/
public Set<AttributedVertex> convertToOutermostVertices(Set<AttributedVertex> vertices) {
Set<AttributedVertex> set = new HashSet<>();
for (AttributedVertex v : vertices) {
set.add(getOutermostVertex(v));
}
return set;
}
/**
* Return the outermost GroupVertex containing the given vertex or else return the given vertex
* if it is not in a group.
*
* @param vertex the vertex to check if inside a group.
* @return the outermost GroupVertex containing the given vertex or else return the given vertex
* if it is not in a group.
*/
public AttributedVertex getOutermostVertex(AttributedVertex vertex) {
while (!graph.containsVertex(vertex)) {
AttributedVertex owner = findOwnerOf(vertex);
if (owner == null) {
break; // should never happen. not sure what to do here, but don't want to loop forever
}
vertex = owner;
}
return vertex;
}
}

View file

@ -0,0 +1,109 @@
/* ###
* 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 java.util.stream.Collectors;
import ghidra.service.graph.AttributedVertex;
/**
* AttributedVertex class to represent a group of "collapsed nodes"
*/
public class GroupVertex extends AttributedVertex {
private static final int MAX_IDS_TO_COMBINE = 6;
private Set<AttributedVertex> children;
private AttributedVertex first;
/**
* Creates a new GroupVertex that represents the grouping of the given vertices.
* @param vertices the nodes to be grouped.
* @return a new GroupVertex.
*/
public static GroupVertex groupVertices(Collection<AttributedVertex> vertices) {
// the set of vertices given may include group nodes, we only want "real nodes"
Set<AttributedVertex> set = flatten(vertices);
List<AttributedVertex> list = new ArrayList<>(set);
Collections.sort(list, Comparator.comparing(AttributedVertex::getName));
return new GroupVertex(set, getUniqueId(list), list.get(0));
}
private GroupVertex(Set<AttributedVertex> children, String id, AttributedVertex first) {
super(id);
this.first = first;
this.children = children;
setAttribute("VertexType", "Collapsed");
setAttribute("Icon", "Star");
}
/**
* Returns a set of vertices such that all non-group nodes in the given vertices are included
* and any group nodes in the given vertices are replaced with their contained vertices.
*
* @param vertices the collection of vertices to flatten into a set of non-group vertices.
* @return a set of non-group vertices derived from the given collection where all the group
* vertices have been replace with their contained vertices.
*/
public static Set<AttributedVertex> flatten(Collection<AttributedVertex> vertices) {
Set<AttributedVertex> set = new HashSet<>();
for (AttributedVertex vertex : vertices) {
if (vertex instanceof GroupVertex) {
set.addAll(((GroupVertex) vertex).children);
}
else {
set.add(vertex);
}
}
return set;
}
private static String getUniqueId(List<AttributedVertex> vertexList) {
if (vertexList.size() > MAX_IDS_TO_COMBINE) {
int idsNotShownCount = vertexList.size() - MAX_IDS_TO_COMBINE;
return combineIds(vertexList.subList(0, MAX_IDS_TO_COMBINE)) + ",..., + " +
idsNotShownCount +
" Others";
}
return combineIds(vertexList);
}
private static String combineIds(Collection<AttributedVertex> vertices) {
return vertices.stream().map(AttributedVertex::getName).collect(Collectors.joining(","));
}
/**
* Returns the set of flattened nodes contained in this node. In other words, any group nodes
* that were given to this group node would have been swapped for the nodes that the groupd node
* contained.
*
* @return the set of flattened graph vertices represented by this group node.
*/
public Set<AttributedVertex> getContainedVertices() {
return Collections.unmodifiableSet(children);
}
/**
* Returns the node that is first, with first being currently defined to be the one that is
* first when sorted by id alphabetically.
*
* @return the node that is first.
*/
public AttributedVertex getFirst() {
return first;
}
}

View file

@ -15,18 +15,18 @@
*/
package ghidra.graph.visualization;
import com.google.common.base.Splitter;
import ghidra.service.graph.Attributed;
import ghidra.service.graph.AttributedEdge;
import static org.jungrapht.visualization.VisualizationServer.*;
import java.awt.*;
import java.util.Map;
import org.apache.commons.text.StringEscapeUtils;
import org.jungrapht.visualization.util.ShapeFactory;
import java.awt.BasicStroke;
import java.awt.Shape;
import java.awt.Stroke;
import java.util.Map;
import com.google.common.base.Splitter;
import static org.jungrapht.visualization.VisualizationServer.PREFIX;
import ghidra.service.graph.Attributed;
import ghidra.service.graph.AttributedEdge;
/**
* a container for various functions used by ProgramGraph
@ -129,10 +129,15 @@ abstract class ProgramGraphFunctions {
*/
public static String getLabel(Attributed attributed) {
Map<String, String> map = attributed.getAttributeMap();
if (map.get("Code") != null) {
if (map.containsKey("Code")) {
String code = StringEscapeUtils.escapeHtml4(map.get("Code"));
return "<html>" + String.join("<p>", Splitter.on('\n').split(code));
}
if ("Collapsed".equals(map.get("VertexType"))) {
String name = StringEscapeUtils.escapeHtml4(map.get("Name"));
return "<html>" + String.join("<p>",
Splitter.on(',').split(name));
}
return map.get("Name");
}
}

View file

@ -78,6 +78,12 @@ public class AttributedEdge extends Attributed {
return id;
}
@Override
public String setAttribute(String key, String value) {
clearCache();
return super.setAttribute(key, value);
}
@Override
public int hashCode() {
return id.hashCode();
@ -97,4 +103,9 @@ public class AttributedEdge extends Attributed {
AttributedEdge other = (AttributedEdge) obj;
return id.equals(other.id);
}
private void clearCache() {
this.htmlString = null;
}
}

View file

@ -17,11 +17,10 @@ package ghidra.service.graph;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.text.StringEscapeUtils;
import java.util.Set;
/**
* Graph vertex with attributes
*/
@ -57,6 +56,12 @@ public class AttributedVertex extends Attributed {
setAttribute("Name", name);
}
@Override
public String setAttribute(String key, String value) {
clearCache();
return super.setAttribute(key, value);
}
/**
* Returns the id for this vertex
* @return the id for this vertex
@ -79,7 +84,7 @@ public class AttributedVertex extends Attributed {
return getName() + " (" + id + ")";
}
public void clearCache() {
private void clearCache() {
this.htmlString = null;
}

View file

@ -28,18 +28,19 @@ import ghidra.app.plugin.core.graph.GraphDisplayBrokerPlugin;
import ghidra.app.services.GraphDisplayBroker;
import ghidra.framework.plugintool.PluginTool;
import ghidra.graph.visualization.DefaultGraphDisplayComponentProvider;
import ghidra.graph.visualization.GroupVertex;
import ghidra.service.graph.*;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.task.TaskMonitor;
public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
private List<String> listenerCalls = new ArrayList<>();
private TestEnv env;
private PluginTool tool;
private AttributedGraph graph;
private ComponentProvider graphComponentProvider;
private GraphDisplay display;
private GraphSpy graphSpy = new GraphSpy();
private AttributedVertex a;
private AttributedVertex b;
private AttributedVertex c;
@ -245,6 +246,194 @@ public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
assertFalse(contains(newGraph, "F"));
}
@Test
public void testCollapseVertices() {
assertEquals(6, display.getGraph().getVertexCount());
select(a, b, c);
collapse();
assertEquals(4, graph.getVertexCount());
GroupVertex groupVertex = findGroupVertex();
Set<AttributedVertex> containedVertices = groupVertex.getContainedVertices();
assertEquals(3, containedVertices.size());
assertTrue(containedVertices.contains(a));
assertTrue(containedVertices.contains(b));
assertTrue(containedVertices.contains(c));
}
@Test
public void testExpandVertices() {
assertEquals(6, display.getGraph().getVertexCount());
select(a, b, c);
collapse();
assertEquals(4, graph.getVertexCount());
GroupVertex groupVertex = findGroupVertex();
assertNotNull(groupVertex);
select(groupVertex);
expand();
assertEquals(6, graph.getVertexCount());
groupVertex = findGroupVertex();
assertNull(groupVertex);
}
@Test
public void testSelectNodeThatIsGrouped() {
select(a, b, c);
collapse();
clearSelection();
assertTrue(display.getSelectedVertices().isEmpty());
// 'b' is inside the group, selecting 'b' should select the group node
select(b);
Set<AttributedVertex> selectedVertices = display.getSelectedVertices();
assertEquals(1, selectedVertices.size());
AttributedVertex vertex = selectedVertices.iterator().next();
assertTrue(vertex instanceof GroupVertex);
}
@Test
public void testSelectNodeThatIsDoubleGrouped() {
select(a, b, c);
collapse();
select(findGroupVertex(), d);
collapse();
clearSelection();
assertTrue(display.getSelectedVertices().isEmpty());
select(b);
Set<AttributedVertex> selectedVertices = display.getSelectedVertices();
assertEquals(1, selectedVertices.size());
AttributedVertex vertex = selectedVertices.iterator().next();
assertTrue(vertex instanceof GroupVertex);
assertEquals(4, ((GroupVertex) vertex).getContainedVertices().size());
}
@Test
public void testFocusNodeThatIsGrouped() {
select(a, b, c);
collapse();
clearSelection();
assertTrue(display.getSelectedVertices().isEmpty());
setFocusedVertex(b);
AttributedVertex vertex = display.getFocusedVertex();
assertTrue(vertex instanceof GroupVertex);
}
@Test
public void testFocusNodeThatIsDoubleGrouped() {
select(a, b, c);
collapse();
select(findGroupVertex(), d);
collapse();
setFocusedVertex(e);
assertEquals(e, display.getFocusedVertex());
setFocusedVertex(b);
AttributedVertex vertex = display.getFocusedVertex();
assertTrue(vertex instanceof GroupVertex);
assertEquals(4, ((GroupVertex) vertex).getContainedVertices().size());
}
@Test
public void testListenerNotificatinWhenGroupNodeFocused() {
select(a, b, c);
collapse();
GroupVertex group = findGroupVertex();
setFocusedVertex(e);
graphSpy.clear();
setFocusedVertex(group, true);
waitForSwing();
assertTrue(graphSpy.isFocused(a));
}
@Test
public void testListenerNotificatinWhenDoubleGroupedNodeFocused() {
select(a, b, c);
collapse();
select(findGroupVertex(), d);
collapse();
GroupVertex group = findGroupVertex();
setFocusedVertex(e);
graphSpy.clear();
setFocusedVertex(group, true);
waitForSwing();
assertTrue(graphSpy.isFocused(a));
}
@Test
public void testSelectNotificatinWhenGroupNodeFocused() {
select(a, b, c);
collapse();
GroupVertex group = findGroupVertex();
clearSelection();
graphSpy.clear();
selectFromGui(group);
waitForSwing();
assertTrue(graphSpy.isSelected(a, b, c));
}
@Test
public void testSelectNotificatinWhenDoubleGroupedNodeFocused() {
select(a, b, c);
collapse();
select(findGroupVertex(), d);
collapse();
GroupVertex group = findGroupVertex();
clearSelection();
graphSpy.clear();
selectFromGui(group);
waitForSwing();
assertTrue(graphSpy.isSelected(a, b, c, d));
}
private void clearSelection() {
select();
}
private void collapse() {
DockingActionIf action = getAction(tool, "Collapse Selected");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
}
private void expand() {
DockingActionIf action = getAction(tool, "Expand Selected");
GraphActionContext context =
new GraphActionContext(graphComponentProvider, graph, null, null);
performAction(action, context, true);
}
private GroupVertex findGroupVertex() {
for (AttributedVertex vertex : graph.vertexSet()) {
if (vertex instanceof GroupVertex) {
return (GroupVertex) vertex;
}
}
return null;
}
private boolean contains(AttributedGraph g, String vertexId) {
return g.getVertex(vertexId) != null;
}
@ -264,8 +453,20 @@ public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
});
}
private void selectFromGui(AttributedVertex... vertices) {
runSwing(() -> {
Set<AttributedVertex> vetexSet = new HashSet<>(Arrays.asList(vertices));
display.selectVertices(vetexSet, EventTrigger.GUI_ACTION);
});
}
private void setFocusedVertex(AttributedVertex vertex) {
runSwing(() -> display.setFocusedVertex(vertex, EventTrigger.INTERNAL_ONLY));
setFocusedVertex(vertex, false);
}
private void setFocusedVertex(AttributedVertex vertex, boolean fireEvent) {
EventTrigger trigger = fireEvent ? EventTrigger.GUI_ACTION : EventTrigger.INTERNAL_ONLY;
runSwing(() -> display.setFocusedVertex(vertex, trigger));
}
class TestGraphDisplayListener implements GraphDisplayListener {
@ -278,24 +479,17 @@ public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
@Override
public void graphClosed() {
listenerCalls.add(name + ": graph closed");
// do nothing
}
@Override
public void selectionChanged(Set<AttributedVertex> verrtices) {
StringBuilder buf = new StringBuilder();
buf.append(name);
buf.append(": selected: ");
for (AttributedVertex vertex : verrtices) {
buf.append(vertex.getId());
buf.append(",");
}
listenerCalls.add(buf.toString());
public void selectionChanged(Set<AttributedVertex> vertices) {
graphSpy.setSelection(vertices);
}
@Override
public void locationFocusChanged(AttributedVertex vertex) {
listenerCalls.add(name + ": focus: " + vertex.getId());
graphSpy.focusChanged(vertex);
}
@Override
@ -305,6 +499,34 @@ public class GraphActionTest extends AbstractGhidraHeadedIntegrationTest {
}
class GraphSpy {
AttributedVertex focusedVertex;
Set<AttributedVertex> selectedVertices;
public void focusChanged(AttributedVertex vertex) {
this.focusedVertex = vertex;
}
public boolean isSelected(AttributedVertex... vertices) {
Set<AttributedVertex> expected = new HashSet<>(Arrays.asList(vertices));
return expected.equals(selectedVertices);
}
public boolean isFocused(AttributedVertex a) {
return a == focusedVertex;
}
public void clear() {
focusedVertex = null;
selectedVertices = null;
}
public void setSelection(Set<AttributedVertex> vertices) {
this.selectedVertices = vertices;
}
}
private AttributedGraph createGraph() {
AttributedGraph g = new AttributedGraph();
a = g.addVertex("A");