From a04185e942c4bed995ed436ca0a70610069165e2 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Thu, 25 Jul 2019 18:40:21 -0400 Subject: [PATCH] GT-3019 - Function Graph - Added option for edge routing around vertices --- .../SampleGraphPluginDependencyLayout.java | 6 +- ...leGraphPluginDependencyLayoutProvider.java | 5 +- .../core/functiongraph/FGActionManager.java | 25 +- .../functiongraph/FunctionGraphPlugin.java | 51 +- .../graph/layout/AbstractFGLayout.java | 4 +- .../graph/layout/EmptyLayout.java | 4 +- .../layout/ExperimentalLayoutProvider.java | 2 +- .../graph/layout/FGLayoutOptions.java | 43 + .../graph/layout/FGLayoutProvider.java | 22 +- .../mvc/FunctionGraphOptions.java | 39 +- .../AbstractFunctionGraphTest.java | 5 - .../graph/layout/TestFGLayoutProvider.java | 23 +- .../graph/layout/DNLayoutOptions.java | 53 ++ .../graph/layout/DecompilerNestedLayout.java | 741 ++++++++++++++---- .../DecompilerNestedLayoutProvider.java | 16 +- .../graph/layout/BowTieLayout.java | 6 +- .../graph/layout/BowTieLayoutProvider.java | 2 +- .../main/java/docking/menu/ActionState.java | 90 ++- ...ualGraphEdgeSelectionGraphMousePlugin.java | 12 +- .../layout/AbstractVisualGraphLayout.java | 17 +- .../viewer/layout/LayoutLocationMap.java | 13 + .../graph/viewer/layout/LayoutProvider.java | 1 - .../viewer/renderer/VisualGraphRenderer.java | 10 +- ...TestGraphAlgorithmSteppingViewerPanel.java | 2 +- 24 files changed, 933 insertions(+), 259 deletions(-) create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutOptions.java create mode 100644 Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLayoutOptions.java diff --git a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayout.java b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayout.java index 4ecfd25e16..b15c843f6a 100644 --- a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayout.java +++ b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayout.java @@ -32,8 +32,8 @@ import ghidra.util.exception.CancelledException; public class SampleGraphPluginDependencyLayout extends AbstractVisualGraphLayout { - protected SampleGraphPluginDependencyLayout(SampleGraph graph) { - super(graph); + protected SampleGraphPluginDependencyLayout(SampleGraph graph, String name) { + super(graph, name); } @Override @@ -50,7 +50,7 @@ public class SampleGraphPluginDependencyLayout } SampleGraphPluginDependencyLayout newLayout = - new SampleGraphPluginDependencyLayout((SampleGraph) newGraph); + new SampleGraphPluginDependencyLayout((SampleGraph) newGraph, getLayoutName()); return newLayout; } diff --git a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayoutProvider.java b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayoutProvider.java index 07933e10a8..612f7a18cb 100644 --- a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayoutProvider.java +++ b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/graph/layout/SampleGraphPluginDependencyLayoutProvider.java @@ -30,20 +30,21 @@ import resources.ResourceManager; public class SampleGraphPluginDependencyLayoutProvider extends AbstractLayoutProvider { + private static final String NAME = "Plugin Dependency Layout"; private static final Icon DEFAULT_ICON = ResourceManager.loadImage("images/color_swatch.png"); @Override public VisualGraphLayout getLayout(SampleGraph g, TaskMonitor monitor) throws CancelledException { - SampleGraphPluginDependencyLayout layout = new SampleGraphPluginDependencyLayout(g); + SampleGraphPluginDependencyLayout layout = new SampleGraphPluginDependencyLayout(g, NAME); initVertexLocations(g, layout); return layout; } @Override public String getLayoutName() { - return "Plugin Dependency Layout"; + return NAME; } // Note: each provider really should load its own icon so that the toolbar item can diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java index e3c16caa45..9c6ce63b66 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java @@ -48,7 +48,6 @@ import ghidra.program.model.listing.Function; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; import ghidra.util.*; -import ghidra.util.exception.AssertException; import resources.Icons; import resources.ResourceManager; @@ -72,7 +71,6 @@ class FGActionManager { private MultiStateDockingAction vertexHoverModeAction; private MultiStateDockingAction vertexFocusModeAction; - private FGLayoutFinder layoutFinder = new DiscoverableFGLayoutFinder(); private MultiStateDockingAction> layoutAction; FGActionManager(FunctionGraphPlugin plugin, FGController controller, FGProvider provider) { @@ -857,8 +855,7 @@ class FGActionManager { HelpLocation layoutHelpLocation = new HelpLocation("FunctionGraphPlugin", "Function_Graph_Action_Layout"); - layoutAction = new MultiStateDockingAction>( - "Relayout Graph", plugin.getName()) { + layoutAction = new MultiStateDockingAction<>("Relayout Graph", plugin.getName()) { @Override protected void doActionPerformed(ActionContext context) { @@ -901,20 +898,12 @@ class FGActionManager { private List>> loadActionStatesForLayoutProviders() { - Set instances = layoutFinder.findLayouts(); - if (instances.isEmpty()) { - throw new AssertException("Could not find any layout providers. You project may not " + - "be configured properly."); - } - - List layoutInstances = new ArrayList<>(instances); - Collections.sort(layoutInstances, - (o1, o2) -> -o1.getPriorityLevel() + o2.getPriorityLevel()); - + List layoutInstances = plugin.getLayoutProviders(); List>> list = new ArrayList<>(); HelpLocation layoutHelpLocation = new HelpLocation("FunctionGraphPlugin", "Function_Graph_Action_Layout"); for (FGLayoutProvider layout : layoutInstances) { + ActionState> layoutState = new ActionState<>( layout.getLayoutName(), layout.getActionIcon(), layout.getClass()); layoutState.setHelpLocation(layoutHelpLocation); @@ -996,7 +985,7 @@ class FGActionManager { offState.setHelpLocation(pathHelpLocation); vertexHoverModeAction = - new MultiStateDockingAction("Block Hover Mode", plugin.getName()) { + new MultiStateDockingAction<>("Block Hover Mode", plugin.getName()) { @Override public void actionStateChanged(ActionState newActionState, @@ -1065,7 +1054,7 @@ class FGActionManager { offState.setHelpLocation(pathHelpLocation); vertexFocusModeAction = - new MultiStateDockingAction("Block Focus Mode", plugin.getName()) { + new MultiStateDockingAction<>("Block Focus Mode", plugin.getName()) { @Override public void actionStateChanged(ActionState newActionState, @@ -1204,10 +1193,6 @@ class FGActionManager { } } - void setLayoutFinder(FGLayoutFinder layoutFinder) { - this.layoutFinder = layoutFinder; - } - void setEdgeFocusMode(EdgeDisplayType edgeDisplayType) { vertexFocusModeAction.setCurrentActionStateByUserData(edgeDisplayType); } diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin.java index 25bfe2826c..2eedda7e7d 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin.java @@ -27,6 +27,8 @@ import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.ProgramPlugin; import ghidra.app.plugin.core.colorizer.ColorizingService; +import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutOptions; +import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutProvider; import ghidra.app.plugin.core.functiongraph.mvc.FunctionGraphOptions; import ghidra.app.services.*; import ghidra.app.util.viewer.format.FormatManager; @@ -40,6 +42,7 @@ import ghidra.graph.viewer.options.VisualGraphOptions; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; +import ghidra.util.exception.AssertException; import resources.ResourceManager; //@formatter:off @@ -78,6 +81,7 @@ public class FunctionGraphPlugin extends ProgramPlugin implements OptionsChangeL private FunctionGraphOptions functionGraphOptions = new FunctionGraphOptions(); private FGColorProvider colorProvider; + private List layoutProviders; public FunctionGraphPlugin(PluginTool tool) { super(tool, true, true, true); @@ -88,6 +92,9 @@ public class FunctionGraphPlugin extends ProgramPlugin implements OptionsChangeL @Override protected void init() { super.init(); + + layoutProviders = loadLayoutProviders(); + createNewProvider(); initializeOptions(); @@ -125,17 +132,44 @@ public class FunctionGraphPlugin extends ProgramPlugin implements OptionsChangeL } } + private List loadLayoutProviders() { + + FGLayoutFinder layoutFinder = new DiscoverableFGLayoutFinder(); + Set instances = layoutFinder.findLayouts(); + if (instances.isEmpty()) { + throw new AssertException("Could not find any layout providers. You project may not " + + "be configured properly."); + } + + List layouts = new ArrayList<>(instances); + Collections.sort(layouts, (o1, o2) -> -o1.getPriorityLevel() + o2.getPriorityLevel()); + return layouts; + } + private void initializeOptions() { ToolOptions options = tool.getOptions(PLUGIN_OPTIONS_NAME); options.addOptionsChangeListener(this); - functionGraphOptions.initializeOptions(this, options); - functionGraphOptions.loadOptions(this, options); + functionGraphOptions.registerOptions(options); + functionGraphOptions.loadOptions(options); + + for (FGLayoutProvider layoutProvider : layoutProviders) { + String layoutName = layoutProvider.getLayoutName(); + Options layoutToolOptions = options.getOptions(layoutName); + FGLayoutOptions layoutOptions = layoutProvider.createLayoutOptions(layoutToolOptions); + if (layoutOptions == null) { + continue; // many layouts do not have options + } + + layoutOptions.registerOptions(layoutToolOptions); + layoutOptions.loadOptions(layoutToolOptions); + functionGraphOptions.setLayoutOptions(layoutName, layoutOptions); + } } @Override public void optionsChanged(ToolOptions options, String optionName, Object oldValue, Object newValue) { - functionGraphOptions.loadOptions(this, options); + functionGraphOptions.loadOptions(options); connectedProvider.getComponent().repaint(); for (FGProvider provider : disconnectedProviders) { provider.getComponent().repaint(); @@ -383,14 +417,6 @@ public class FunctionGraphPlugin extends ProgramPlugin implements OptionsChangeL } } - @Override - public void dataStateRestoreCompleted() { - super.dataStateRestoreCompleted(); -// ProgramLocation location = ProgramLocation.getLocation( -// currentProgram, dataSaveState, null ); - - } - public FGColorProvider getColorProvider() { return colorProvider; } @@ -399,4 +425,7 @@ public class FunctionGraphPlugin extends ProgramPlugin implements OptionsChangeL return functionGraphOptions; } + public List getLayoutProviders() { + return Collections.unmodifiableList(layoutProviders); + } } diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/AbstractFGLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/AbstractFGLayout.java index c725ffa961..77638aa192 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/AbstractFGLayout.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/AbstractFGLayout.java @@ -37,8 +37,8 @@ public abstract class AbstractFGLayout extends AbstractVisualGraphLayout implements FGLayout { + private static final String NAME = "Empty Layout"; + public EmptyLayout(FunctionGraph graph) { - super(graph); + super(graph, NAME); } @Override diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java index 9a08a608f1..0844e72bc8 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java @@ -19,7 +19,7 @@ import javax.swing.Icon; import resources.ResourceManager; -public abstract class ExperimentalLayoutProvider implements FGLayoutProvider { +public abstract class ExperimentalLayoutProvider extends FGLayoutProvider { private static final Icon ICON = ResourceManager.loadImage("images/package_development.png"); diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutOptions.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutOptions.java new file mode 100644 index 0000000000..547e5ca3d4 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutOptions.java @@ -0,0 +1,43 @@ +/* ### + * 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.app.plugin.core.functiongraph.graph.layout; + +import ghidra.app.plugin.core.functiongraph.FunctionGraphPlugin; +import ghidra.framework.options.Options; + +/** + * An interface for {@link FGLayout} options + */ +public interface FGLayoutOptions { + + public static final String OWNER = FunctionGraphPlugin.class.getSimpleName(); + + /** + * Called during setup for this class to register its options with the given {@link Options} + * object + * + * @param options the tool options + */ + public void registerOptions(Options options); + + /** + * Called when the given {@link Options} object has changed. This class will update its + * options with the values from the given options object. + * + * @param options the tool options + */ + public void loadOptions(Options options); +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutProvider.java index 4ab9017e25..d239783761 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutProvider.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutProvider.java @@ -18,20 +18,30 @@ package ghidra.app.plugin.core.functiongraph.graph.layout; import ghidra.app.plugin.core.functiongraph.graph.FGEdge; import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; +import ghidra.framework.options.Options; import ghidra.graph.viewer.layout.LayoutProvider; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -public interface FGLayoutProvider extends LayoutProvider { +public abstract class FGLayoutProvider implements LayoutProvider { + + public abstract FGLayout getFGLayout(FunctionGraph graph, TaskMonitor monitor) + throws CancelledException; // Suppressing warning on the return type; we know our class is the right type @Override - public default FGLayout getLayout(FunctionGraph graph, TaskMonitor monitor) - throws CancelledException { - + public FGLayout getLayout(FunctionGraph graph, TaskMonitor monitor) throws CancelledException { return getFGLayout(graph, monitor); } - public FGLayout getFGLayout(FunctionGraph graph, TaskMonitor monitor) throws CancelledException; - + /** + * Creates an options object for layouts created by this provider. Returns null if there + * are not options for layouts created by this provider. + * + * @param options the tool options into which layout options should be registered + * @return the new options; null if there are no options + */ + public FGLayoutOptions createLayoutOptions(Options options) { + return null; + } } diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/mvc/FunctionGraphOptions.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/mvc/FunctionGraphOptions.java index f0fc972174..880710d799 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/mvc/FunctionGraphOptions.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/mvc/FunctionGraphOptions.java @@ -16,15 +16,20 @@ package ghidra.app.plugin.core.functiongraph.mvc; import java.awt.Color; +import java.util.*; +import java.util.Map.Entry; -import ghidra.framework.options.ToolOptions; -import ghidra.framework.plugintool.Plugin; +import ghidra.app.plugin.core.functiongraph.FunctionGraphPlugin; +import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutOptions; +import ghidra.framework.options.Options; import ghidra.graph.viewer.options.*; import ghidra.program.model.symbol.FlowType; import ghidra.util.HelpLocation; public class FunctionGraphOptions extends VisualGraphOptions { + protected static final String OWNER = FunctionGraphPlugin.class.getSimpleName(); + private static final String EDGE_FALLTHROUGH_HIGHLIGHT_COLOR_KEY = "Edge Color - Fallthrough Highlight"; private static final String EDGE_UNCONDITIONAL_JUMP_HIGHLIGHT_COLOR_KEY = @@ -48,7 +53,7 @@ public class FunctionGraphOptions extends VisualGraphOptions { private static final String DEFAULT_GROUP_BACKGROUND_COLOR_KEY = "Default Group Color"; private static final String DEFAULT_GROUP_BACKGROUND_COLOR_DESCRPTION = - "The default " + "background color applied to newly created group vertices"; + "The default background color applied to newly created group vertices"; private static final String UPDATE_GROUP_AND_UNGROUP_COLORS = "Update Vertex Colors When Grouping"; @@ -74,6 +79,8 @@ public class FunctionGraphOptions extends VisualGraphOptions { protected RelayoutOption relayoutOption = RelayoutOption.NEVER; + private Map layoutOptionsByName = new HashMap<>(); + public Color getDefaultGroupBackgroundColor() { return defaultGroupBackgroundColor; } @@ -110,8 +117,9 @@ public class FunctionGraphOptions extends VisualGraphOptions { return relayoutOption; } - public void initializeOptions(Plugin plugin, ToolOptions options) { - HelpLocation help = new HelpLocation(plugin.getName(), "Options"); + public void registerOptions(Options options) { + + HelpLocation help = new HelpLocation(OWNER, "Options"); options.setOptionsHelpLocation(help); options.registerOption(RELAYOUT_OPTIONS_KEY, RelayoutOption.VERTEX_GROUPING_CHANGES, help, @@ -124,8 +132,7 @@ public class FunctionGraphOptions extends VisualGraphOptions { USE_MOUSE_RELATIVE_ZOOM_DESCRIPTION); options.registerOption(USE_CONDENSED_LAYOUT, useCondensedLayout(), - new HelpLocation(plugin.getName(), "Layout_Compressing"), - USE_CONDENSED_LAYOUT_DESCRIPTION); + new HelpLocation(OWNER, "Layout_Compressing"), USE_CONDENSED_LAYOUT_DESCRIPTION); options.registerOption(VIEW_RESTORE_OPTIONS_KEY, ViewRestoreOption.START_FULLY_ZOOMED_OUT, help, VIEW_RESTORE_OPTIONS_DESCRIPTION); @@ -161,7 +168,7 @@ public class FunctionGraphOptions extends VisualGraphOptions { } - public void loadOptions(Plugin plugin, ToolOptions options) { + public void loadOptions(Options options) { conditionalJumpEdgeColor = options.getColor(EDGE_COLOR_CONDITIONAL_JUMP_KEY, conditionalJumpEdgeColor); @@ -198,6 +205,14 @@ public class FunctionGraphOptions extends VisualGraphOptions { updateGroupColorsAutomatically = options.getBoolean(UPDATE_GROUP_AND_UNGROUP_COLORS, updateGroupColorsAutomatically); + + Set> entries = layoutOptionsByName.entrySet(); + for (Entry entry : entries) { + String layoutName = entry.getKey(); + FGLayoutOptions layoutOptions = entry.getValue(); + Options layoutToolOptions = options.getOptions(layoutName); + layoutOptions.loadOptions(layoutToolOptions); + } } public Color getColor(FlowType flowType) { @@ -227,4 +242,12 @@ public class FunctionGraphOptions extends VisualGraphOptions { return Color.BLACK; } + + public FGLayoutOptions getLayoutOptions(String layoutName) { + return layoutOptionsByName.get(layoutName); + } + + public void setLayoutOptions(String layoutName, FGLayoutOptions options) { + layoutOptionsByName.put(layoutName, options); + } } diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java index 1aa088fb71..1f23fa3e2b 100644 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java +++ b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java @@ -53,7 +53,6 @@ import ghidra.app.plugin.core.clipboard.ClipboardPlugin; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.functiongraph.graph.*; import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutProvider; -import ghidra.app.plugin.core.functiongraph.graph.layout.TestFGLayoutProvider; import ghidra.app.plugin.core.functiongraph.graph.vertex.*; import ghidra.app.plugin.core.functiongraph.mvc.*; import ghidra.app.services.*; @@ -75,7 +74,6 @@ import ghidra.program.util.ProgramSelection; import ghidra.test.*; import ghidra.util.Msg; import ghidra.util.task.RunManager; -import util.CollectionUtils; public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedIntegrationTest { @@ -558,9 +556,6 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte graphProvider = waitForComponentProvider(FGProvider.class); assertNotNull("Graph not shown", graphProvider); - - FGActionManager actionManager = graphProvider.getActionManager(); - actionManager.setLayoutFinder(() -> CollectionUtils.asSet(new TestFGLayoutProvider())); } protected ProgramSelection makeSingleVertexSelectionInCodeBrowser() { diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/TestFGLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/TestFGLayoutProvider.java index 0b9b1d3196..af3235cc0d 100644 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/TestFGLayoutProvider.java +++ b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/TestFGLayoutProvider.java @@ -25,6 +25,7 @@ import javax.swing.Icon; import ghidra.app.plugin.core.functiongraph.graph.FGEdge; import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; +import ghidra.app.plugin.core.functiongraph.graph.vertex.GroupedFunctionGraphVertex; import ghidra.graph.VisualGraph; import ghidra.graph.viewer.layout.*; import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; @@ -37,13 +38,14 @@ import resources.Icons; /** * A simple layout that is used during testing */ -public class TestFGLayoutProvider implements FGLayoutProvider { +public class TestFGLayoutProvider extends FGLayoutProvider { + private static final String NAME = "Test Layout"; private static final int VERTEX_TO_EDGE_ARTICULATION_OFFSET = 20; @Override public String getLayoutName() { - return "Test Layout"; + return NAME; } @Override @@ -65,7 +67,7 @@ public class TestFGLayoutProvider implements FGLayoutProvider { private class TestFGLayout extends AbstractFGLayout { protected TestFGLayout(FunctionGraph graph) { - super(graph); + super(graph, NAME); } @Override @@ -224,7 +226,9 @@ public class TestFGLayoutProvider implements FGLayoutProvider { treeify(g, left, nodesByVertices); break; default: - Msg.debug(this, "\n\n\tMore than 2 edges????: " + parent); + if (!(parent.v instanceof GroupedFunctionGraphVertex)) { + Msg.debug(this, "\n\n\tMore than 2 edges????: " + parent); + } } } @@ -357,5 +361,16 @@ public class TestFGLayoutProvider implements FGLayoutProvider { Node(FGVertex v) { this.v = v; } + + @Override + public String toString() { + //@formatter:off + return "{\n" + + "\tv: " + v + ",\n" + + "\tleft: " + left + ",\n" + + "\tright: " + right + "\n" + + "}"; + //@formatter:on + } } } diff --git a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLayoutOptions.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLayoutOptions.java new file mode 100644 index 0000000000..fe331c080b --- /dev/null +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLayoutOptions.java @@ -0,0 +1,53 @@ +/* ### + * 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.app.plugin.core.functiongraph.graph.layout; + +import ghidra.framework.options.Options; +import ghidra.util.HelpLocation; + +/** + * Options for the {@link DecompilerNestedLayout} + */ +public class DNLayoutOptions implements FGLayoutOptions { + + private static final String USE_EDGE_ROUTING_AROUND_VERTICES_KEY = + "Route Edges Around Vertices"; + private static final String USE_EDGE_ROUTING_AROUND_VERTICES_DESCRIPTION = "Signals that " + + "edges should be routed around any intersecting vertex. When toggled off, edges will " + + "pass through any intersecting vertices."; + + private boolean useEdgeRoutingAroundVertices; + + @Override + public void registerOptions(Options options) { + + // TODO layout-specific help + HelpLocation help = new HelpLocation(OWNER, "Options"); + + options.registerOption(USE_EDGE_ROUTING_AROUND_VERTICES_KEY, useEdgeRoutingAroundVertices, + help, USE_EDGE_ROUTING_AROUND_VERTICES_DESCRIPTION); + } + + @Override + public void loadOptions(Options options) { + useEdgeRoutingAroundVertices = + options.getBoolean(USE_EDGE_ROUTING_AROUND_VERTICES_KEY, useEdgeRoutingAroundVertices); + } + + public boolean useEdgeRoutingAroundVertices() { + return useEdgeRoutingAroundVertices; + } +} diff --git a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java index 79ba4d88aa..bff28d7aaa 100644 --- a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java @@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap; +import org.apache.commons.collections4.map.LazyMap; import com.google.common.base.Function; @@ -49,24 +50,46 @@ import ghidra.util.exception.AssertException; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -// TODO paint fallthrough differently for all, or just for those returning to the baseline -// TODO: edges for loops could stand out more...maybe not needed with better routing or background painting - -// TODO: should we allow grouping in this layout? - -// TODO: entry not always at the top - winhello.exe 402c8c +/** + * A layout that uses the decompiler to show code nesting based upon conditional logic. + * + *

Edges returning to the default code flow are painted lighter to de-emphasize them. This + * could be made into an option. + * + * TODO ideas: + * -paint fallthrough differently for all, or just for those returning to the baseline + * -using 'edge routing mode' that avoids vertices can be tweaked so that all returning edges also + * will not overlap (do this by keeping some sort of mapping for a given edge's column); when + * not routing edges around vertices, the edges intentionally overlap, to reduce noise + */ public class DecompilerNestedLayout extends AbstractFGLayout { - private static final int VERTEX_TO_EDGE_ARTICULATION_OFFSET = 20; + /** Amount of visual buffer between edges and other things used to show separation */ + private static final int EDGE_SPACING = 5; + + /** The space between an articulation point and its vertex */ + private static final int VERTEX_TO_EDGE_ARTICULATION_PADDING = 20; + + private static final int VERTEX_TO_EDGE_AVOIDANCE_PADDING = + VERTEX_TO_EDGE_ARTICULATION_PADDING - EDGE_SPACING; + + /** Multiplier used to grow spacing as distance between two edge endpoints grows */ + private static final int EDGE_ENDPOINT_DISTANCE_MULTIPLIER = 20; + + /** Amount to keep an edge away from the bounding box of a vertex */ + private static final int VERTEX_BORDER_THICKNESS = EDGE_SPACING; + + /** An amount by which edges entering a vertex from the left are offset to avoid overlapping */ + private static final int EDGE_OFFSET_INCOMING_FROM_LEFT = EDGE_SPACING; private DecompilerBlockGraph blockGraphRoot; - public DecompilerNestedLayout(FunctionGraph graph) { - this(graph, true); + public DecompilerNestedLayout(FunctionGraph graph, String name) { + this(graph, name, true); } - private DecompilerNestedLayout(FunctionGraph graph, boolean initialize) { - super(graph); + private DecompilerNestedLayout(FunctionGraph graph, String name, boolean initialize) { + super(graph, name); if (initialize) { initialize(); } @@ -89,6 +112,10 @@ public class DecompilerNestedLayout extends AbstractFGLayout { return .3; } + private DNLayoutOptions getLayoutOptions() { + return (DNLayoutOptions) options.getLayoutOptions(getLayoutName()); + } + @Override protected GridLocationMap performInitialGridLayout( VisualGraph jungGraph) throws CancelledException { @@ -190,15 +217,25 @@ public class DecompilerNestedLayout extends AbstractFGLayout { } } - // TODO The 'vertexLayoutLocations' is too close to 'layoutLocations'...rename/refactor @Override protected Map> positionEdgeArticulationsInLayoutSpace( VisualGraphVertexShapeTransformer transformer, Map vertexLayoutLocations, Collection edges, - LayoutLocationMap layoutLocations) throws CancelledException { + LayoutLocationMap layoutToGridMap) throws CancelledException { Map> newEdgeArticulations = new HashMap<>(); + // Condensing Note: we have guilty knowledge that our parent class my condense the + // vertices and edges towards the center of the graph after we calculate positions. + // To prevent the edges from moving to far behind the vertices, we will compensate a + // bit for that effect using this offset value. The getEdgeOffset() method below is + // updated for the condense factor. + int edgeOffset = isCondensedLayout() + ? (int) (VERTEX_TO_EDGE_ARTICULATION_PADDING * (1 - getCondenseFactor())) + : VERTEX_TO_EDGE_ARTICULATION_PADDING; + Vertex2dFactory vertex2dFactory = + new Vertex2dFactory(transformer, vertexLayoutLocations, layoutToGridMap, edgeOffset); + // // Route our edges! // @@ -208,134 +245,453 @@ public class DecompilerNestedLayout extends AbstractFGLayout { FGVertex startVertex = e.getStart(); FGVertex endVertex = e.getEnd(); + Vertex2d start = vertex2dFactory.get(startVertex); + Vertex2d end = vertex2dFactory.get(endVertex); + Address startAddress = startVertex.getVertexAddress(); Address endAddress = endVertex.getVertexAddress(); - int result = startAddress.compareTo(endAddress); - DecompilerBlock block = blockGraphRoot.getBlock(endVertex); - DecompilerBlock loop = block.getParentLoop(); + int compareResult = startAddress.compareTo(endAddress); + boolean goingUp = compareResult > 0; - if (result > 0 && loop != null) { - // TODO better check for loops - routeLoopEdge(vertexLayoutLocations, layoutLocations, newEdgeArticulations, e, - startVertex, endVertex); + if (goingUp) { + + DecompilerBlock block = blockGraphRoot.getBlock(endVertex); + DecompilerBlock loop = block.getParentLoop(); + + if (loop != null) { + Set vertices = loop.getVertices(); + + Column outermostCol = getOutermostCol(layoutToGridMap, vertices); + Column loopEndColumn = layoutToGridMap.nextColumn(outermostCol); + List articulations = routeLoopEdge(start, end, loopEndColumn); + newEdgeArticulations.put(e, articulations); + + vertex2dFactory.dispose(); + return newEdgeArticulations; + } } - else { - // - // TODO For now I will use the layout positions to determine edge type (nested v. - // fallthrough). It would be nicer if I had this information defined somewhere - // -->Maybe positioning is simple enough? - // - Column startCol = layoutLocations.col(startVertex); - Column endCol = layoutLocations.col(endVertex); - Point2D start = vertexLayoutLocations.get(startVertex); - Point2D end = vertexLayoutLocations.get(endVertex); - List articulations = new ArrayList<>(); + List articulations = new ArrayList<>(); - int direction = 20; - if (startCol.index < endCol.index) { // going forward on the x-axis -// TODO make constant -// direction = 10; - } - else if (startCol.index > endCol.index) { // going backwards on the x-axis - direction = -direction; - } + // + // Basic routing: + // -leave the bottom of the start vertex + // -first bend at some constant offset + // -move to right or left, to above the end vertex + // -second bend above the end vertex at previous constant offset + // + // Edges start/end on the vertex center. If we offset them to avoid + // overlapping, then they produce angles when only using two articulations. + // Thus, we create articulations that are behind the vertices to remove + // the angles. This points will not be seen. + // + // + // Complex routing: + // -this mode will route edges around vertices + // + // One goal for complex edge routing is to prevent overlapping (simple edge routing + // prefers overlapping to reduce lines). To prevent overlapping we will use different + // offset x and y values, depending upon the start and end vertex row and column + // locations. Specifically, for a given edge direction there will be a bias: + // -Edge to the right - leave from the right; arrive to the left + // -Edge to the left - leave from the left; arrive to the right + // -Edge straight down - go straight down + // + // For each of the above offsets, there will be an amplifier based upon row/column + // distance from start to end vertex. This has the effect that larger vertex + // distances will have a larger offset/spacing. + // - int offsetFromVertex = isCondensedLayout() - ? (int) (VERTEX_TO_EDGE_ARTICULATION_OFFSET * (1 - getCondenseFactor())) - : VERTEX_TO_EDGE_ARTICULATION_OFFSET; + if (start.columnIndex < end.columnIndex) { // going to the right - if (startCol.index < endCol.index) { // going left or right - // - // Basic routing: - // -leave the bottom of the start vertex - // -first bend at some constant offset - // -move to right or left, to above the end vertex - // -second bend above the end vertex at previous constant offset - // - // Advanced considerations: - // -Remove angles from vertex points: - // -->Edges start/end on the vertex center. If we offset them to avoid - // overlapping, then they produce angles when only using two articulations. - // Thus, we will create articulations that are behind the vertices to remove - // the angles. This points will not be seen. - // - Shape shape = transformer.apply(startVertex); - Rectangle bounds = shape.getBounds(); - double vertexBottom = start.getY() + (bounds.height >> 1); // location is centered - - double x1 = start.getX() + direction; - double y1 = start.getY(); // hidden - articulations.add(new Point2D.Double(x1, y1)); - - double x2 = x1; - double y2 = vertexBottom + offsetFromVertex; - y2 = end.getY(); - articulations.add(new Point2D.Double(x2, y2)); - - double x3 = end.getX() + (-direction); - double y3 = y2; - articulations.add(new Point2D.Double(x3, y3)); - -// double x4 = x3; -// double y4 = end.getY(); // hidden -// articulations.add(new Point2D.Double(x4, y4)); - } - - else if (startCol.index > endCol.index) { // flow return - e.setAlpha(.25); - - Shape shape = transformer.apply(startVertex); - Rectangle bounds = shape.getBounds(); - double vertexBottom = start.getY() + (bounds.height >> 1); // location is centered - - double x1 = start.getX() + (direction); - double y1 = start.getY(); // hidden - articulations.add(new Point2D.Double(x1, y1)); - - double x2 = x1; - double y2 = vertexBottom + offsetFromVertex; - articulations.add(new Point2D.Double(x2, y2)); - - double x3 = end.getX() + (-direction); - double y3 = y2; - articulations.add(new Point2D.Double(x3, y3)); - - double x4 = x3; - double y4 = end.getY(); // hidden - articulations.add(new Point2D.Double(x4, y4)); - } - - else { // same column--nothing to route - // straight line, which is the default - e.setAlpha(.25); - } - newEdgeArticulations.put(e, articulations); + routeToTheRight(start, end, vertex2dFactory, articulations); } + + else if (start.columnIndex > end.columnIndex) { // going to the left; flow return + + // check for the up or down direction + if (start.rowIndex < end.rowIndex) { // down + routeToTheLeft(start, end, e, vertex2dFactory, articulations); + } + else { + routeToTheRightGoingUpwards(start, end, vertex2dFactory, articulations); + } + } + + else { // going down; no nesting; flow return + + routeDownward(start, end, e, vertex2dFactory, articulations); + } + + newEdgeArticulations.put(e, articulations); } + vertex2dFactory.dispose(); return newEdgeArticulations; } - private void routeLoopEdge(Map vertexLayoutLocations, - LayoutLocationMap layoutLocations, - Map> newEdgeArticulations, FGEdge e, FGVertex startVertex, - FGVertex endVertex) { + private void routeToTheRightGoingUpwards(Vertex2d start, Vertex2d end, + Vertex2dFactory vertex2dFactory, List articulations) { + + // + // For routing to the right and back up we will leave the start vertex from the right side + // and enter the end vertex on the right side. As the vertices get further apart, we will + // space them further in towards the center. + // + + int delta = start.rowIndex - end.rowIndex; + int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + + // Condensing Note: we have guilty knowledge that our parent class my condense the + // vertices and edges towards the center of the graph after we calculate positions. + // To prevent the edges from moving to far behind the vertices, we will compensate a + // bit for that effect using this offset value. The getEdgeOffset() method is + // updated for the condense factor. + int exaggerationFactor = 1; + if (isCondensedLayout()) { + exaggerationFactor = 2; // determined by trial-and-error; can be made into an option + } + distanceSpacing *= exaggerationFactor; + + double x1 = start.getX(); + double y1 = start.getTop() + VERTEX_BORDER_THICKNESS; + + // spacing moves closer to center as the distance grows + y1 += distanceSpacing; + + // restrict y from moving past the center + double startCenterY = start.getY() - VERTEX_BORDER_THICKNESS; + y1 = Math.min(y1, startCenterY); + articulations.add(new Point2D.Double(x1, y1)); // point is hidden behind the vertex + + // Use the spacing to move the y value towards the top of the vertex. Just like with + // the x value, restrict the y to the range between the edge and the center. + double startRightX = start.getRight(); + double x2 = startRightX + VERTEX_BORDER_THICKNESS; // start at the end + + // spacing moves closer to center as the distance grows + x2 += distanceSpacing; + + double y2 = y1; + articulations.add(new Point2D.Double(x2, y2)); + + double x3 = x2; + double y3 = end.getBottom() - VERTEX_BORDER_THICKNESS; + + // spacing moves closer to center as the distance grows + y3 -= distanceSpacing; + + // restrict from moving back past the center + double endYLimit = end.getY() + VERTEX_BORDER_THICKNESS; + y3 = Math.max(y3, endYLimit); + articulations.add(new Point2D.Double(x3, y3)); + + Column column = vertex2dFactory.getColumn(x1); + routeAroundColumnVertices(start, end, column.index, vertex2dFactory, articulations, x3); + + double x4 = end.getX(); + double y4 = y3; + articulations.add(new Point2D.Double(x4, y4)); // point is hidden behind the vertex + } + + private void routeDownward(Vertex2d start, Vertex2d end, FGEdge e, + Vertex2dFactory vertex2dFactory, List articulations) { + + lighten(e); + + int delta = end.rowIndex - start.rowIndex; + int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + + double x1 = start.getX() - distanceSpacing; // update for extra spacing + double y1 = start.getY(); // hidden + articulations.add(new Point2D.Double(x1, y1)); + + double x2 = x1; // same distance over + double y2 = end.getY(); + articulations.add(new Point2D.Double(x2, y2)); + + double x3 = end.getX() + (-distanceSpacing); + double y3 = y2; + + routeAroundColumnVertices(start, end, vertex2dFactory, articulations, x3); + + articulations.add(new Point2D.Double(x3, y3)); + } + + private void routeToTheLeft(Vertex2d start, Vertex2d end, FGEdge e, + Vertex2dFactory vertex2dFactory, List articulations) { + + lighten(e); + + // + // For routing to the left we will leave the start vertex from just left of center and + // enter the end vertex on the top, towards the right. As the vertices get further apart, + // we will space them further in towards the center of the end vertex. This will keep + // edges with close endpoints from intersecting edges with distant endpoints. + // + + int delta = end.rowIndex - start.rowIndex; + int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + + double x1 = start.getX() - VERTEX_BORDER_THICKNESS; // start at the center + + // spacing moves closer to left edge as the distance grows + x1 -= distanceSpacing; + + // restrict from moving backwards past the edge + double startXLimit = start.getLeft() + VERTEX_BORDER_THICKNESS; + x1 = Math.max(x1, startXLimit); + + // restrict x from moving past the end vertex x value to force the edge to enter + // from the side + double endRightX = end.getRight() - VERTEX_BORDER_THICKNESS; + x1 = Math.max(x1, endRightX); + + double y1 = start.getY(); + articulations.add(new Point2D.Double(x1, y1)); // point is hidden behind the vertex + + double x2 = x1; + double y2 = start.getBottom() + start.getEdgeOffset(); + articulations.add(new Point2D.Double(x2, y2)); // out of the bottom of the vertex + + // Use the spacing to move the end x value towards the center of the vertex + double x3 = endRightX - VERTEX_BORDER_THICKNESS; // start at the end + + // spacing moves closer to center as the distance grows + x3 -= distanceSpacing; + + // restrict x from moving past the end vertex center x + int edgeOffset = 0; + if (getLayoutOptions().useEdgeRoutingAroundVertices()) { + // for now, only offset edge lines when we are performing complex routing + edgeOffset = EDGE_OFFSET_INCOMING_FROM_LEFT; + } + double endXLimit = end.getX() + VERTEX_BORDER_THICKNESS + edgeOffset; + x3 = Math.max(x3, endXLimit); + double y3 = y2; + articulations.add(new Point2D.Double(x3, y3)); // into the top of the end vertex + + routeAroundColumnVertices(start, end, vertex2dFactory, articulations, x3); + + double x4 = x3; + double y4 = end.getY(); + articulations.add(new Point2D.Double(x4, y4)); // point is hidden behind the vertex + } + + private void routeToTheRight(Vertex2d start, Vertex2d end, Vertex2dFactory vertex2dFactory, + List articulations) { + + // + // For routing to the right we will leave the start vertex from the right side and + // enter the end vertex on the left side. As the vertices get further apart, we will + // space them further in towards the center. This will keep edges with close endpoints + // from intersecting edges with distant endpoints. + // + + int delta = end.rowIndex - start.rowIndex; + int distanceSpacing = delta * EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + + double startRightX = start.getRight(); + double x1 = startRightX - VERTEX_BORDER_THICKNESS; // start at the end + + // spacing moves closer to center as the distance grows + x1 -= distanceSpacing; + + // restrict x from moving past the end vertex x value to force the edge to enter + // from the side + double endLeftX = end.getLeft() - end.getEdgeOffset(); + x1 = Math.min(x1, endLeftX); + + // restrict from moving backwards past the center + double startXLimit = start.getX() + VERTEX_BORDER_THICKNESS; + x1 = Math.max(x1, startXLimit); + + double y1 = start.getY(); + articulations.add(new Point2D.Double(x1, y1)); // point is hidden behind the vertex + + // Use the spacing to move the y value towards the top of the vertex. Just like with + // the x value, restrict the y to the range between the edge and the center. + double x2 = x1; + double y2 = end.getTop() + VERTEX_BORDER_THICKNESS; + + // spacing moves closer to center as the distance grows + y2 += distanceSpacing; + + // restrict from moving forwards past the center + double endYLimit = end.getY() - VERTEX_BORDER_THICKNESS; + y2 = Math.min(y2, endYLimit); + articulations.add(new Point2D.Double(x2, y2)); + + // have not yet seen an example of vertex/edge clipping when routing to the right + // routeAroundColumnVertices(start, end, vertex2dFactory, articulations, x2); + + double x3 = end.getX(); + double y3 = y2; + articulations.add(new Point2D.Double(x3, y3)); // point is hidden behind the vertex + } + + private void routeAroundColumnVertices(Vertex2d start, Vertex2d end, + Vertex2dFactory vertex2dFactory, List articulations, double edgeX) { + + int column = end.columnIndex; + routeAroundColumnVertices(start, end, column, vertex2dFactory, articulations, edgeX); + } + + private void routeAroundColumnVertices(Vertex2d start, Vertex2d end, int column, + Vertex2dFactory vertex2dFactory, List articulations, double edgeX) { + + if (!getLayoutOptions().useEdgeRoutingAroundVertices()) { + return; + } + + int startRow = start.rowIndex; + int endRow = end.rowIndex; + + if (startRow > endRow) { // going upwards + endRow = start.rowIndex; + startRow = end.rowIndex; + } + + for (int row = startRow + 1; row < endRow; row++) { + + // assume any other vertex in our column can clip (it will not clip when + // the 'spacing' above pushes the edge away from this column, like for + // large row delta values) + Vertex2d otherVertex = vertex2dFactory.get(row, column); + if (otherVertex == null) { + continue; // no vertex in this cell + } + + int delta = endRow - startRow; + int padding = VERTEX_TO_EDGE_AVOIDANCE_PADDING; + int distanceSpacing = padding + delta; // adding the delta makes overlap less likely + + // Condensing Note: we have guilty knowledge that our parent class my condense the + // vertices and edges towards the center of the graph after we calculate positions. + // To prevent the edges from moving to far behind the vertices, we will compensate a + // bit for that effect using this offset value. The getEdgeOffset() method is + // updated for the condense factor. + int vertexToEdgeOffset = otherVertex.getEdgeOffset(); + int exaggerationFactor = 1; + if (isCondensedLayout()) { + exaggerationFactor = 4; // determined by trial-and-error; can be made into an option + } + + double centerX = otherVertex.getX(); + boolean isLeft = edgeX < centerX; + + // no need to check the 'y' value, as the end vertex is below this one + + if (isLeft) { + if (edgeX >= otherVertex.getLeft()) { + // less than center; after the start of the vertex + + /* + Must route around this vertex - new points: + -p1 - just above the intersection point + -p2 - just past the left edge + -p3 - just past the bottom of the vertex + -p4 - back at the original x value + + | + .___| + | .-----. + | | | + | '-----' + '---. + | + */ + + // p1 - same x; y just above vertex + double x = edgeX; + double y = otherVertex.getTop() - vertexToEdgeOffset; + articulations.add(new Point2D.Double(x, y)); + + // maybe merge points if they are too close together + if (articulations.size() > 2) { + Point2D previousArticulation = articulations.get(articulations.size() - 2); + int closenessHeight = 50; + double previousY = previousArticulation.getY(); + if (y - previousY < closenessHeight) { + // remove our first articulation and the one before that so that + // the get merged into on line on the outside of the vertex + articulations.remove(articulations.size() - 1); + articulations.remove(articulations.size() - 1); + Point2D newPrevious = articulations.get(articulations.size() - 1); + y = newPrevious.getY(); + } + } + + // p2 - move over; same y + int offset = Math.max(vertexToEdgeOffset, distanceSpacing); + offset *= exaggerationFactor; + x = otherVertex.getLeft() - offset; + articulations.add(new Point2D.Double(x, y)); + + // p3 - same x; move y below the vertex + y = otherVertex.getBottom() + vertexToEdgeOffset; + articulations.add(new Point2D.Double(x, y)); + + // p4 - move over back to our original x; same y + x = edgeX; + articulations.add(new Point2D.Double(x, y)); + } + } + else { // right + + double otherEnd = otherVertex.getRight(); + if (edgeX < otherEnd) { + // greater than center; before the end of the vertex + // (see not above for routing info) + + // p1 - same x; y just above vertex + double x = edgeX; + double y = otherVertex.getTop() - vertexToEdgeOffset; + articulations.add(new Point2D.Double(x, y)); + + // maybe merge points if they are too close together + if (articulations.size() > 2) { + Point2D previousArticulation = articulations.get(articulations.size() - 2); + int closenessHeight = 50; + double previousY = previousArticulation.getY(); + if (y - previousY < closenessHeight) { + // remove our first articulation and the one before that so that + // the get merged into on line on the outside of the vertex + articulations.remove(articulations.size() - 1); + articulations.remove(articulations.size() - 1); + Point2D newPrevious = articulations.get(articulations.size() - 1); + y = newPrevious.getY(); + } + } + + // p2 - move over; same y + int offset = Math.max(vertexToEdgeOffset, distanceSpacing); + offset *= exaggerationFactor; + x = otherEnd + offset; + articulations.add(new Point2D.Double(x, y)); + + // p3 - same x; move y below the vertex + y = otherVertex.getBottom() + vertexToEdgeOffset; + articulations.add(new Point2D.Double(x, y)); + + // p4 - move over back to our original x; same y + x = edgeX; + articulations.add(new Point2D.Double(x, y)); + } + } + } + } + + private List routeLoopEdge(Vertex2d start, Vertex2d end, Column loopEndColumn) { + // going backwards List articulations = new ArrayList<>(); - DecompilerBlock block = blockGraphRoot.getBlock(endVertex); - DecompilerBlock loop = block.getParentLoop(); - Set vertices = loop.getVertices(); - // loop first point - same y coord as the vertex; x is the middle of the next col - Column outermostCol = getOutermostCol(layoutLocations, vertices); - Column afterColumn = layoutLocations.nextColumn(outermostCol); + int halfWidth = loopEndColumn.getPaddedWidth(isCondensedLayout()) >> 1; + double x = loopEndColumn.x + halfWidth; // middle of the column - int halfWidth = afterColumn.getPaddedWidth(isCondensedLayout()) >> 1; - double x = afterColumn.x + halfWidth; // middle of the column - - Point2D startVertexPoint = vertexLayoutLocations.get(startVertex); + Point2D startVertexPoint = start.center; double y1 = startVertexPoint.getY(); Point2D first = new Point2D.Double(x, y1); @@ -344,12 +700,20 @@ public class DecompilerNestedLayout extends AbstractFGLayout { // loop second point - same y coord as destination; // x is the col after the outermost dominated vertex - Point2D endVertexPoint = vertexLayoutLocations.get(endVertex); + Point2D endVertexPoint = end.center; double y2 = endVertexPoint.getY(); Point2D second = new Point2D.Double(x, y2); articulations.add(second); - newEdgeArticulations.put(e, articulations); + return articulations; + } + + private void lighten(FGEdge e) { + + // assumption: edges that move to the left in this layout are return flows that happen + // after the code block has been executed. We dim those a bit so that they + // produce less clutter. + e.setAlpha(.25); } private Column getOutermostCol(LayoutLocationMap layoutLocations, @@ -566,13 +930,130 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override protected AbstractVisualGraphLayout createClonedFGLayout( FunctionGraph newGraph) { - return new DecompilerNestedLayout(newGraph, false); + return new DecompilerNestedLayout(newGraph, getLayoutName(), false); } //================================================================================================== // Inner Classes //================================================================================================== + private class Vertex2dFactory { + + private VisualGraphVertexShapeTransformer vertexShaper; + private Map vertexLayoutLocations; + private LayoutLocationMap layoutToGridMap; + private int edgeOffset; + private Map cache = + LazyMap.lazyMap(new HashMap<>(), v -> new Vertex2d(v, vertexShaper, + vertexLayoutLocations, layoutToGridMap, getEdgeOffset())); + + Vertex2dFactory(VisualGraphVertexShapeTransformer transformer, + Map vertexLayoutLocations, + LayoutLocationMap layoutToGridMap, int edgeOffset) { + this.vertexShaper = transformer; + this.vertexLayoutLocations = vertexLayoutLocations; + this.layoutToGridMap = layoutToGridMap; + this.edgeOffset = edgeOffset; + } + + Column getColumn(double x) { + return layoutToGridMap.getColumnContaining((int) x); + } + + private int getEdgeOffset() { + return edgeOffset; + } + + Vertex2d get(FGVertex v) { + return cache.get(v); + } + + Vertex2d get(int rowIndex, int columnIndex) { + + Row row = layoutToGridMap.row(rowIndex); + FGVertex v = row.getVertex(columnIndex); + if (v == null) { + return null; + } + return get(v); + } + + void dispose() { + cache.clear(); + } + } + + /** + * A class that represents 2D information about the contained vertex, such as location, + * bounds, row and column of the layout grid. + */ + private class Vertex2d { + + private FGVertex v; + private Row row; + private Column column; + private int rowIndex; + private int columnIndex; + private Point2D center; // center point of vertex shape + private Shape shape; + private Rectangle bounds; // centered over the 'location' + private int edgeOffset; + + Vertex2d(FGVertex v, VisualGraphVertexShapeTransformer transformer, + Map vertexLayoutLocations, + LayoutLocationMap layoutLocations, int edgeOffset) { + + this.v = v; + this.row = layoutLocations.row(v); + this.rowIndex = row.index; + this.column = layoutLocations.col(v); + this.columnIndex = column.index; + this.center = vertexLayoutLocations.get(v); + this.shape = transformer.apply(v); + this.bounds = shape.getBounds(); + this.edgeOffset = edgeOffset; + + // center bounds over location (this is how the graph gets painted) + double cornerX = center.getX() + bounds.getWidth() / 2; + double cornerY = center.getY() + bounds.getHeight() / 2; + Point2D corner = new Point2D.Double(cornerX, cornerY); + bounds.setFrameFromCenter(center, corner); + } + + double getY() { + return center.getY(); + } + + double getX() { + return center.getX(); + } + + double getLeft() { + return center.getX() - (bounds.width >> 1); + } + + double getRight() { + return center.getX() + (bounds.width >> 1); + } + + double getBottom() { + return center.getY() + (bounds.height >> 1); + } + + double getTop() { + return center.getY() - (bounds.height >> 1); + } + + int getEdgeOffset() { + return edgeOffset; + } + + @Override + public String toString() { + return v.toString(); + } + } + private class DecompilerBlockGraph extends DecompilerBlock { protected List allChildren = new ArrayList<>(); @@ -666,7 +1147,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { return null; -// return "Block"; TODO could put in a 'debug name' method for debugging } @Override @@ -834,13 +1314,13 @@ public class DecompilerNestedLayout extends AbstractFGLayout { case LIST: return new ListBlock(parent, block); case CONDITION: - return new ConditionBlock(parent, block); // TODO - not sure + return new ConditionBlock(parent, block); // not sure case PROPERIF: return new IfBlock(parent, block); case IFELSE: return new IfElseBlock(parent, block); case IFGOTO: - return new IfBlock(parent, block); // TODO - not sure + return new IfBlock(parent, block); // not sure case WHILEDO: return new WhileLoopBlock(parent, block); case DOWHILE: @@ -874,7 +1354,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { - return "Plain"; // TODO: maybe just null + return "Plain"; } } @@ -902,7 +1382,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { return parent.getName(); -// return "List"; } } @@ -967,7 +1446,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { block.setCol(column); } - doSetCol(col); // TODO does the non-copy block need a column?? + doSetCol(col); } @Override diff --git a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayoutProvider.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayoutProvider.java index 072f3dd6f0..259ddbe3ed 100644 --- a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayoutProvider.java +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayoutProvider.java @@ -18,25 +18,33 @@ package ghidra.app.plugin.core.functiongraph.graph.layout; import javax.swing.Icon; import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; +import ghidra.framework.options.Options; import ghidra.util.task.TaskMonitor; import resources.ResourceManager; -public class DecompilerNestedLayoutProvider implements FGLayoutProvider { +public class DecompilerNestedLayoutProvider extends FGLayoutProvider { private static final Icon ICON = ResourceManager.loadImage("images/function_graph_code_flow.png"); + static final String LAYOUT_NAME = "Nested Code Layout"; @Override public FGLayout getFGLayout(FunctionGraph graph, TaskMonitor monitor) { - DecompilerNestedLayout layout = new DecompilerNestedLayout(graph); + DecompilerNestedLayout layout = new DecompilerNestedLayout(graph, LAYOUT_NAME); layout.setTaskMonitor(monitor); return layout; } + @Override + public FGLayoutOptions createLayoutOptions(Options options) { + DNLayoutOptions layoutOptions = new DNLayoutOptions(); + layoutOptions.registerOptions(options); + return layoutOptions; + } + @Override public String getLayoutName() { - // TODO better name?...or rename classes to match - return "Nested Code Layout"; + return LAYOUT_NAME; } @Override diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java index b2c7c522e3..e4b1dc2982 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java @@ -44,8 +44,8 @@ import ghidra.util.task.TaskMonitor; */ public class BowTieLayout extends AbstractVisualGraphLayout { - protected BowTieLayout(FunctionCallGraph graph) { - super(graph); + protected BowTieLayout(FunctionCallGraph graph, String name) { + super(graph, name); } @Override @@ -58,7 +58,7 @@ public class BowTieLayout extends AbstractVisualGraphLayout getClass().getSimpleName()); } - BowTieLayout newLayout = new BowTieLayout((FunctionCallGraph) newGraph); + BowTieLayout newLayout = new BowTieLayout((FunctionCallGraph) newGraph, getLayoutName()); return newLayout; } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayoutProvider.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayoutProvider.java index 6dcde9ad7f..3d5985820a 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayoutProvider.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayoutProvider.java @@ -38,7 +38,7 @@ public class BowTieLayoutProvider public VisualGraphLayout getLayout(FunctionCallGraph graph, TaskMonitor monitor) throws CancelledException { - BowTieLayout layout = new BowTieLayout(graph); + BowTieLayout layout = new BowTieLayout(graph, NAME); initVertexLocations(graph, layout); return layout; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java index 3ceceb9929..d5a64456ec 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/ActionState.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +15,17 @@ */ package docking.menu; +import javax.swing.Icon; + import ghidra.util.HelpLocation; import ghidra.util.SystemUtilities; -import javax.swing.Icon; - /** - * Note: this class overrides the {@ #equals(Object)} method} and relies upon the equals + * Note: this class overrides the equals(Object) and relies upon the equals * method of the userData object. Thus, if it is important that equals work for you in - * the non-standard identity way, then you must override equals in your user data obejcts. + * the non-standard identity way, then you must override equals in your user data objects. + * + * @param the type of the action state */ public class ActionState { @@ -32,55 +33,60 @@ public class ActionState { private final Icon icon; private T userData; private HelpLocation helpLocation; - + public ActionState(String name, Icon icon, T userData) { this.name = name; this.icon = icon; - this.userData = userData; + this.userData = userData; } - + public String getName() { return name; } - + public Icon getIcon() { return icon; } - + public T getUserData() { return userData; } - - public void setHelpLocation( HelpLocation helpLocation ) { - this.helpLocation = helpLocation; - } - - public HelpLocation getHelpLocation() { - return helpLocation; - } - @Override - public boolean equals( Object other ) { - if ( other == null ) { - return false; - } - - Class otherClass = other.getClass(); - if ( !getClass().equals( otherClass ) ) { - return false; - } - - ActionState otherState = (ActionState) other; - - if ( !SystemUtilities.isEqual( userData, otherState.userData ) ) { - return false; - } - - return name.equals( otherState.name ); - } - - @Override - public int hashCode() { - return name.hashCode() + ((userData == null) ? 0 : userData.hashCode()); - } + public void setHelpLocation(HelpLocation helpLocation) { + this.helpLocation = helpLocation; + } + + public HelpLocation getHelpLocation() { + return helpLocation; + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + Class otherClass = other.getClass(); + if (!getClass().equals(otherClass)) { + return false; + } + + ActionState otherState = (ActionState) other; + + if (!SystemUtilities.isEqual(userData, otherState.userData)) { + return false; + } + + return name.equals(otherState.name); + } + + @Override + public int hashCode() { + return name.hashCode() + ((userData == null) ? 0 : userData.hashCode()); + } + + @Override + public String toString() { + return name + ": " + userData; + } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEdgeSelectionGraphMousePlugin.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEdgeSelectionGraphMousePlugin.java index 1d338cb93f..ef339c94d9 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEdgeSelectionGraphMousePlugin.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEdgeSelectionGraphMousePlugin.java @@ -76,11 +76,15 @@ public class VisualGraphEdgeSelectionGraphMousePlugin pickedVertexState, GraphViewer viewer) { - GPickedState pickedStateWrapper = (GPickedState) pickedVertexState; - pickedStateWrapper.pickToActivate(vertex); - VisualGraphViewUpdater updater = viewer.getViewUpdater(); - updater.moveVertexToCenterWithAnimation(vertex); + updater.moveVertexToCenterWithAnimation(vertex, isBusy -> { + + // pick the vertex after the animation has run + if (!isBusy) { + GPickedState pickedStateWrapper = (GPickedState) pickedVertexState; + pickedStateWrapper.pickToActivate(vertex); + } + }); } @Override diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java index 9c42ca1e30..f0a8e9ed87 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java @@ -33,7 +33,6 @@ import ghidra.graph.VisualGraph; import ghidra.graph.viewer.*; import ghidra.graph.viewer.layout.LayoutListener.ChangeType; import ghidra.graph.viewer.renderer.ArticulatedEdgeRenderer; -import ghidra.graph.viewer.renderer.VisualGraphRenderer; import ghidra.graph.viewer.shape.ArticulatedEdgeTransformer; import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; import ghidra.util.datastruct.WeakDataStructureFactory; @@ -87,12 +86,22 @@ public abstract class AbstractVisualGraphLayout(); private ArticulatedEdgeRenderer edgeRenderer = new ArticulatedEdgeRenderer<>(); - protected TaskMonitor monitor = TaskMonitor.DUMMY; - + protected String layoutName; protected boolean layoutInitialized; - protected AbstractVisualGraphLayout(Graph graph) { + protected TaskMonitor monitor = TaskMonitor.DUMMY; + + protected AbstractVisualGraphLayout(Graph graph, String layoutName) { super(graph); + this.layoutName = layoutName; + } + + /** + * Returns the name of this layout + * @return the name of this layout + */ + public String getLayoutName() { + return layoutName; } /** diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java index 0caba00e68..42b195a976 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java @@ -99,6 +99,18 @@ public class LayoutLocationMap { return doGetColumn(gridX); } + public Column getColumnContaining(int x) { + Column column = null; + Collection values = columnsByIndex.values(); + for (Column nextColumn : values) { + column = nextColumn; + if (x < column.x) { + return column; + } + } + return column; + } + private Column doGetColumn(int index) { Column column = columnsByIndex.get(index); if (column == null) { @@ -342,4 +354,5 @@ public class LayoutLocationMap { offset += column.getPaddedWidth(isCondensed); } } + } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutProvider.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutProvider.java index 43b5689d8a..cf5715171c 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutProvider.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutProvider.java @@ -48,7 +48,6 @@ public interface LayoutProvider // paint all the edges // DEBUG code to show the edges *over* the vertices -// for (E e : layout.getGraph().getEdges()) { -// -// renderEdge(renderContext, layout, e); -// renderEdgeLabel(renderContext, layout, e); -// } + for (E e : layout.getGraph().getEdges()) { + + renderEdge(renderContext, layout, e); + renderEdgeLabel(renderContext, layout, e); + } paintLayoutGridCells(renderContext, layout); } diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java index 78f172b30c..f83fad28a7 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java @@ -273,7 +273,7 @@ public class TestGraphAlgorithmSteppingViewerPanel> extend AbstractVisualGraphLayout, AlgorithmTestSteppingEdge> { protected TestGraphLayout(TestGraph graph) { - super(graph); + super(graph, "Test Layout"); } @SuppressWarnings("unchecked")