From 724ab5a9502d045235556dab7b0379d2a6168c76 Mon Sep 17 00:00:00 2001 From: ghidravore Date: Fri, 20 Nov 2020 16:48:12 -0500 Subject: [PATCH] removed dead code Incorporates the Jungrapht 'collapse nodes' feature into ghidra graphing. --- .../topics/GraphServices/GraphDisplay.htm | 7 + .../visualization/DefaultGraphDisplay.java | 84 ++++-- .../visualization/GhidraGraphCollapser.java | 109 ++++++++ .../graph/visualization/GroupVertex.java | 109 ++++++++ .../visualization/ProgramGraphFunctions.java | 23 +- .../ghidra/service/graph/AttributedEdge.java | 11 + .../service/graph/AttributedVertex.java | 11 +- .../java/ghidra/graph/GraphActionTest.java | 248 +++++++++++++++++- 8 files changed, 552 insertions(+), 50 deletions(-) create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GhidraGraphCollapser.java create mode 100644 Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GroupVertex.java diff --git a/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm b/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm index ea093ef790..3ebb957878 100644 --- a/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm +++ b/Ghidra/Features/GraphServices/src/main/help/help/topics/GraphServices/GraphDisplay.htm @@ -206,6 +206,13 @@
  • Display Popup Windows - When toggled off no tooltip popups will be displayed.
  • + +
  • + Collapse Selected Vertices - The selected vertices are grouped into a single vertex.
  • + +
  • + Expand Selected Vertices - Any group vertices are reverted back to the vertices that it contains.
  • + diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java index c5747d3f34..bc899db382 100644 --- a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java +++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java @@ -161,6 +161,7 @@ public class DefaultGraphDisplay implements GraphDisplay { private ToggleDockingAction togglePopupsAction; private PopupRegulator popupRegulator; + private GhidraGraphCollapser graphCollapser; /** * Create the initial display, the graph-less visualization viewer, and its controls @@ -442,6 +443,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") @@ -453,6 +468,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(); @@ -679,7 +705,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); @@ -708,23 +734,27 @@ public class DefaultGraphDisplay implements GraphDisplay { return true; } - @SuppressWarnings("unchecked") - private Collection getVertices(Object item) { - if (item instanceof Collection) { - return (Collection) 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 selected) { - Swing.runLater(() -> listener.selectionChanged(selected)); + // replace any group vertices with their individual vertices. + Set flattened = GroupVertex.flatten(selected); + Swing.runLater(() -> listener.selectionChanged(flattened)); + } + + public static Set flatten(Collection vertices) { + Set set = new HashSet<>(); + for (AttributedVertex vertex : vertices) { + if (vertex instanceof GroupVertex) { + set.addAll(((GroupVertex) vertex).getContainedVertices()); + } + else { + set.add(vertex); + } + } + return set; } /** @@ -732,25 +762,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 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 vertices = graphCollapser.convertToOutermostVertices(selected); MutableSelectedState 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(); } @@ -905,6 +936,8 @@ public class DefaultGraphDisplay implements GraphDisplay { graph.addVertex("1", "Graph Aborted"); } doSetGraphData(graph); + graphCollapser = new GhidraGraphCollapser(viewer); + } private AttributedGraph mergeGraphs(AttributedGraph newGraph, AttributedGraph oldGraph) { @@ -998,7 +1031,6 @@ public class DefaultGraphDisplay implements GraphDisplay { @Override public void updateVertexName(AttributedVertex vertex, String newName) { vertex.setName(newName); - vertex.clearCache(); iconCache.evict(vertex); viewer.repaint(); } @@ -1144,7 +1176,7 @@ 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 selectedVertices = getVertices(e.getItem()); + Set selectedVertices = getSelectedVertices(); notifySelectionChanged(new HashSet(selectedVertices)); if (selectedVertices.size() == 1) { @@ -1158,7 +1190,8 @@ public class DefaultGraphDisplay implements GraphDisplay { } } else if (e.getStateChange() == ItemEvent.DESELECTED) { - notifySelectionChanged(Collections.emptySet()); + Set selectedVertices = getSelectedVertices(); + notifySelectionChanged(selectedVertices); } viewer.repaint(); } @@ -1385,4 +1418,5 @@ public class DefaultGraphDisplay implements GraphDisplay { // this graph display does not have a notion of emphasizing } } + } diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GhidraGraphCollapser.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GhidraGraphCollapser.java new file mode 100644 index 0000000000..4b63a3c0be --- /dev/null +++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GhidraGraphCollapser.java @@ -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 { + + public GhidraGraphCollapser(VisualizationServer vv) { + super(vv, null); + } + + @Override + public AttributedVertex collapse(Collection 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 selectedVState = vv.getSelectedVertexState(); + MutableSelectedState selectedEState = vv.getSelectedEdgeState(); + Collection 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 convertToOutermostVertices(Set vertices) { + Set 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; + } +} diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GroupVertex.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GroupVertex.java new file mode 100644 index 0000000000..012b482c17 --- /dev/null +++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/GroupVertex.java @@ -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 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 vertices) { + + // the set of vertices given may include group nodes, we only want "real nodes" + Set set = flatten(vertices); + List list = new ArrayList<>(set); + Collections.sort(list, Comparator.comparing(AttributedVertex::getName)); + return new GroupVertex(set, getUniqueId(list), list.get(0)); + } + + private GroupVertex(Set 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 flatten(Collection vertices) { + Set 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 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 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 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; + } + +} diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/ProgramGraphFunctions.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/ProgramGraphFunctions.java index 283790052f..f82e685117 100644 --- a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/ProgramGraphFunctions.java +++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/ProgramGraphFunctions.java @@ -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 map = attributed.getAttributeMap(); - if (map.get("Code") != null) { + if (map.containsKey("Code")) { String code = StringEscapeUtils.escapeHtml4(map.get("Code")); return "" + String.join("

    ", Splitter.on('\n').split(code)); } + if ("Collapsed".equals(map.get("VertexType"))) { + String name = StringEscapeUtils.escapeHtml4(map.get("Name")); + return "" + String.join("

    ", + Splitter.on(',').split(name)); + } return map.get("Name"); } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java index 2c34b2b8a2..bdb0671aec 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java @@ -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; + } + } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java index bb6c1fd9d3..dded3d3fa0 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java @@ -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; } diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/graph/GraphActionTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/graph/GraphActionTest.java index 39584dd51d..ff22139d45 100644 --- a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/graph/GraphActionTest.java +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/graph/GraphActionTest.java @@ -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 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 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 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 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 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 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 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 selectedVertices; + + public void focusChanged(AttributedVertex vertex) { + this.focusedVertex = vertex; + } + + public boolean isSelected(AttributedVertex... vertices) { + Set 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 vertices) { + this.selectedVertices = vertices; + } + + } + private AttributedGraph createGraph() { AttributedGraph g = new AttributedGraph(); a = g.addVertex("A");