mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-09-18 01:31:53 +00:00
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:
commit
e6fe576b87
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue