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/certification.manifest b/Ghidra/Features/FunctionGraph/certification.manifest index 837c247046..aa2f88ebac 100644 --- a/Ghidra/Features/FunctionGraph/certification.manifest +++ b/Ghidra/Features/FunctionGraph/certification.manifest @@ -19,6 +19,7 @@ src/main/help/help/shared/tip.png||Oxygen Icons - LGPL 3.0|||renamed oxygen file src/main/help/help/shared/undo.png||GHIDRA||reviewed||END| src/main/help/help/shared/warning.png||Oxygen Icons - LGPL 3.0||||END| src/main/help/help/topics/FunctionGraphPlugin/Function_Graph.html||GHIDRA|||contains screenshots of various icons with IP and listing windows|END| +src/main/help/help/topics/FunctionGraphPlugin/Function_Graph_Layouts.html||GHIDRA||||END| src/main/help/help/topics/FunctionGraphPlugin/images/FunctionGraphWindow.png||GHIDRA||||END| src/main/help/help/topics/FunctionGraphPlugin/images/FunctionGraph_Action_Layout.png||GHIDRA||reviewed||END| src/main/help/help/topics/FunctionGraphPlugin/images/FunctionGraph_Action_Show_All_Loops_In_Function.png||GHIDRA||reviewed|created in-house|END| diff --git a/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml b/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml index 79090c4f56..50b978faac 100644 --- a/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml @@ -63,6 +63,9 @@ + + + diff --git a/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph.html b/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph.html index cc0ad06b5d..bc669244be 100644 --- a/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph.html +++ b/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph.html @@ -809,6 +809,11 @@ option to fit as many vertices on the screen as possible. Disable this option to make the overall layout of the graph more aesthetic.

+

The Use Full-size TooltipWhen toggled off, the tooltip for a vertex will be + the same size and layout of the vertices in the graph. When toggled on, the tooltip + for a vertex will be larger, using the layout of the Listing. The larger size is + more informative, but also takes up more space.

+

The Use Mouse-relative Zoom option signals zoom the graph to and from the mouse location when zooming from the middle-mouse. The default for this option is off, which triggers zoom to work from the center of the graph, regardless of the mouse location.

diff --git a/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph_Layouts.html b/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph_Layouts.html new file mode 100644 index 0000000000..2f2eab68a8 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/help/help/topics/FunctionGraphPlugin/Function_Graph_Layouts.html @@ -0,0 +1,59 @@ + + + + + + + Function Graph Plugin + + + + + +

Function Graph Layouts

+ + +
+

Nested Code Layout

+ +
+

The nested code layout use the + Decompiler to arrange the + code blocks of a function in a way that mimics the nesting of the source code as seen + in the decompiled function. As an example, any code block that must pass through an + if statement will be nested below and to the right of the code block that + contains the conditional check. The nested code block is dominated by the block + containing the conditional check--code flow can only reach the nested block by passing + through the block above it. Also, code blocks that represent a default code flow + will be aligned to the left and below other code blocks in the function. This layout + allows the user to quickly see the dominating relationships between code blocks. +

+

The edges leaving a code block are labeled with the type of high-level conditional + statement (e.g., if, if/else, etc) used to determine code flow. +

+

By default, edges are routed such that they are grouped together such that any edges + returning to a shared code block will overlap. This reduces visual clutter at the + expense of being able to visually follow individual edges to their vertices. Another + consequence of this routing is that sometimes edges will travel behind unrelated + vertices, again, making it difficult to visually follow these edges. The edge + routing can be changed via the options below. +

+ + +

Nested Code Layout Options

+ +
+

The Route Edges Around Vertices option triggers this layout to route + edges around any vertex that would otherwise touch that edge. (See above for + notes on how edges are routed for this layout.) +

+
+ +
+ + +

Provided by: Function Graph Plugin

+
+ + \ No newline at end of file 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..04a7c3c209 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(), true) { @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/FGProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java index fe477d581e..176d318061 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java @@ -574,6 +574,17 @@ public class FGProvider extends VisualGraphComponentProvider 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,29 +132,60 @@ 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); - connectedProvider.getComponent().repaint(); - for (FGProvider provider : disconnectedProviders) { - provider.getComponent().repaint(); - } - if (VisualGraphOptions.USE_CONDENSED_LAYOUT.equals(optionName)) { - // the condensed setting requires us to reposition the graph + functionGraphOptions.loadOptions(options); + + if (functionGraphOptions.optionChangeRequiresRelayout(optionName)) { connectedProvider.refreshAndKeepPerspective(); } else if (VisualGraphOptions.VIEW_RESTORE_OPTIONS_KEY.equals(optionName)) { connectedProvider.clearViewSettings(); } + else { + connectedProvider.refreshDisplayWithoutRebuilding(); + } + + connectedProvider.getComponent().repaint(); + for (FGProvider provider : disconnectedProviders) { + provider.getComponent().repaint(); + } } @Override @@ -383,14 +421,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 +429,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/FGComponent.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FGComponent.java index f62295bf68..34bc2ec633 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FGComponent.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FGComponent.java @@ -24,7 +24,6 @@ import org.jdom.Element; import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.visualization.RenderContext; import edu.uci.ics.jung.visualization.picking.PickedState; -import edu.uci.ics.jung.visualization.renderers.DefaultEdgeLabelRenderer; import edu.uci.ics.jung.visualization.renderers.Renderer; import edu.uci.ics.jung.visualization.util.Caching; import ghidra.app.plugin.core.functiongraph.graph.jung.renderer.FGEdgePaintTransformer; @@ -37,6 +36,7 @@ import ghidra.graph.viewer.*; import ghidra.graph.viewer.layout.LayoutListener.ChangeType; import ghidra.graph.viewer.layout.LayoutProvider; import ghidra.graph.viewer.layout.VisualGraphLayout; +import ghidra.graph.viewer.renderer.VisualGraphEdgeLabelRenderer; import ghidra.program.model.listing.Function; import ghidra.program.util.ProgramLocation; import ghidra.util.SystemUtilities; @@ -208,7 +208,12 @@ public class FGComponent extends GraphComponent // edge label rendering com.google.common.base.Function edgeLabelTransformer = e -> e.getLabel(); renderContext.setEdgeLabelTransformer(edgeLabelTransformer); - DefaultEdgeLabelRenderer edgeLabelRenderer = new DefaultEdgeLabelRenderer(Color.ORANGE); + + // note: this label renderer is the stamp for the label; we use another edge label + // renderer inside of the VisualGraphRenderer + VisualGraphEdgeLabelRenderer edgeLabelRenderer = + new VisualGraphEdgeLabelRenderer(Color.BLACK); + edgeLabelRenderer.setNonPickedForegroundColor(Color.LIGHT_GRAY); edgeLabelRenderer.setRotateEdgeLabels(false); renderContext.setEdgeLabelRenderer(edgeLabelRenderer); 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..1d076aa0c4 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/FGLayoutOptions.java @@ -0,0 +1,52 @@ +/* ### + * 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); + + /** + * Returns true if the given option name, when changed, requires that the current graph be + * reloaded for the change to take effect + * + * @param optionName the changed option name + * @return true if a relayout is required + */ + public boolean optionChangeRequiresRelayout(String optionName); +} 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/graph/vertex/ListingGraphComponentPanel.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/ListingGraphComponentPanel.java index f5bc621092..1d5441d98b 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/ListingGraphComponentPanel.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/ListingGraphComponentPanel.java @@ -198,12 +198,23 @@ public class ListingGraphComponentPanel extends AbstractGraphComponentPanel { private void createListingPanelToolTipComponent() { JPanel panel = new JPanel(new BorderLayout()); - previewListingPanel = - new FGVertexListingPanel(controller, getFormatManager(true), program, addressSet); + + FunctionGraphOptions options = controller.getFunctionGraphOptions(); + boolean useFullSizeTooltip = options.useFullSizeTooltip(); + previewListingPanel = new FGVertexListingPanel(controller, + getFormatManager(useFullSizeTooltip), program, addressSet); previewListingPanel.setTextBackgroundColor(FGVertex.TOOLTIP_BACKGROUND_COLOR); // previewListingPanel.getFieldPanel().setSelectionMode( FieldPanel.NO_SELECTION ); previewListingPanel.getFieldPanel().setCursorOn(false); + // keep the tooltip window from getting too big; use an arbitrary, reasonable max + Dimension maxSize = new Dimension(700, 400); + previewListingPanel.setMaximumSize(maxSize); + Dimension preferredSize = previewListingPanel.getPreferredSize(); + preferredSize.width = Math.min(maxSize.width, preferredSize.width); + preferredSize.height = Math.min(maxSize.height, preferredSize.height); + previewListingPanel.setPreferredSize(preferredSize); + tooltipTitleLabel = new GDLabel(); tooltipTitleLabel.setHorizontalAlignment(SwingConstants.LEADING); tooltipTitleLabel.setBackground(FGVertex.TOOLTIP_BACKGROUND_COLOR); @@ -239,6 +250,7 @@ public class ListingGraphComponentPanel extends AbstractGraphComponentPanel { // make sure the title stays up-to-date with the symbol at the start address title = createTitle(); genericHeader.setTitle(title); + previewListingPanel = null; } @Override 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..16208425f9 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 = @@ -36,6 +41,12 @@ public class FunctionGraphOptions extends VisualGraphOptions { "Edge Color - Unconditional Jump "; private static final String EDGE_COLOR_CONDITIONAL_JUMP_KEY = "Edge Color - Conditional Jump "; + private static final String USE_FULL_SIZE_TOOLTIP_KEY = "Use Full-size Tooltip"; + private static final String USE_FULL_SIZE_TOOLTIP_DESCRIPTION = "Signals to use the " + "" + + "full-size vertex inside of the tooltip popup. When enabled the tooltip vertex will " + + "use the same format size as the Listing. When disabled, the vertex will use the " + + "same format size as in the Function Graph."; + public static final String RELAYOUT_OPTIONS_KEY = "Automatic Graph Relayout"; public static final String RELAYOUT_OPTIONS_DESCRIPTION = "Signals to the Function Graph " + "when an automatic relayout of the graph should take place. The basic options are:
    " + @@ -43,12 +54,12 @@ public class FunctionGraphOptions extends VisualGraphOptions { "
  • Block Model Changes Only - relayout the graph when the block model changes " + "(like when a label has been added to the program in the currently graphed function)
  • " + "
  • Vertex Grouping Changes Only - when vertices are grouped or ungrouped
  • " + - "
  • Never - do not automatically relayout the graph
" + "

" + + "
  • Never - do not automatically relayout the graph


  • " + "See help for more"; 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"; @@ -72,7 +83,11 @@ public class FunctionGraphOptions extends VisualGraphOptions { private Color unconditionalJumpEdgeHighlightColor = HOVER_HIGHLIGHT_UNCONDITIONAL_COLOR; private Color conditionalJumpEdgeHighlightColor = HOVER_HIGHLIGHT_CONDITIONAL_COLOR; - protected RelayoutOption relayoutOption = RelayoutOption.NEVER; + private boolean useFullSizeTooltip = false; + + private RelayoutOption relayoutOption = RelayoutOption.NEVER; + + private Map layoutOptionsByName = new HashMap<>(); public Color getDefaultGroupBackgroundColor() { return defaultGroupBackgroundColor; @@ -110,8 +125,13 @@ public class FunctionGraphOptions extends VisualGraphOptions { return relayoutOption; } - public void initializeOptions(Plugin plugin, ToolOptions options) { - HelpLocation help = new HelpLocation(plugin.getName(), "Options"); + public boolean useFullSizeTooltip() { + return useFullSizeTooltip; + } + + public void registerOptions(Options options) { + + HelpLocation help = new HelpLocation(OWNER, "Options"); options.setOptionsHelpLocation(help); options.registerOption(RELAYOUT_OPTIONS_KEY, RelayoutOption.VERTEX_GROUPING_CHANGES, help, @@ -120,12 +140,11 @@ public class FunctionGraphOptions extends VisualGraphOptions { options.registerOption(SHOW_ANIMATION_OPTIONS_KEY, useAnimation(), help, SHOW_ANIMATION_DESCRIPTION); - options.registerOption(USE_MOUSE_RELATIVE_ZOOM, useMouseRelativeZoom(), help, + options.registerOption(USE_MOUSE_RELATIVE_ZOOM_KEY, useMouseRelativeZoom(), help, USE_MOUSE_RELATIVE_ZOOM_DESCRIPTION); - options.registerOption(USE_CONDENSED_LAYOUT, useCondensedLayout(), - new HelpLocation(plugin.getName(), "Layout_Compressing"), - USE_CONDENSED_LAYOUT_DESCRIPTION); + options.registerOption(USE_CONDENSED_LAYOUT_KEY, useCondensedLayout(), + 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); @@ -139,6 +158,9 @@ public class FunctionGraphOptions extends VisualGraphOptions { options.registerOption(UPDATE_GROUP_AND_UNGROUP_COLORS, updateGroupColorsAutomatically, help, UPDATE_GROUP_AND_UNGROUP_COLORS_DESCRIPTION); + options.registerOption(USE_FULL_SIZE_TOOLTIP_KEY, useFullSizeTooltip, help, + USE_FULL_SIZE_TOOLTIP_DESCRIPTION); + options.registerOption(EDGE_COLOR_CONDITIONAL_JUMP_KEY, conditionalJumpEdgeColor, help, "Conditional jump edge color"); @@ -161,7 +183,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); @@ -184,9 +206,12 @@ public class FunctionGraphOptions extends VisualGraphOptions { useAnimation = options.getBoolean(SHOW_ANIMATION_OPTIONS_KEY, useAnimation); - useMouseRelativeZoom = options.getBoolean(USE_MOUSE_RELATIVE_ZOOM, useMouseRelativeZoom); + useMouseRelativeZoom = + options.getBoolean(USE_MOUSE_RELATIVE_ZOOM_KEY, useMouseRelativeZoom); - useCondensedLayout = options.getBoolean(USE_CONDENSED_LAYOUT, useCondensedLayout); + useCondensedLayout = options.getBoolean(USE_CONDENSED_LAYOUT_KEY, useCondensedLayout); + + useFullSizeTooltip = options.getBoolean(USE_FULL_SIZE_TOOLTIP_KEY, useFullSizeTooltip); viewRestoreOption = options.getEnum(VIEW_RESTORE_OPTIONS_KEY, ViewRestoreOption.START_FULLY_ZOOMED_OUT); @@ -198,6 +223,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 +260,29 @@ public class FunctionGraphOptions extends VisualGraphOptions { return Color.BLACK; } + + public boolean optionChangeRequiresRelayout(String optionName) { + if (USE_CONDENSED_LAYOUT_KEY.equals(optionName)) { + return true; + } + + Set> entries = layoutOptionsByName.entrySet(); + for (Entry entry : entries) { + FGLayoutOptions layoutOptions = entry.getValue(); + if (layoutOptions.optionChangeRequiresRelayout(optionName)) { + return true; + } + } + + return false; + } + + 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/CodeFlowEdgeLabelRenderer.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/CodeFlowEdgeLabelRenderer.java deleted file mode 100644 index 4861c70a25..0000000000 --- a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/CodeFlowEdgeLabelRenderer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* ### - * 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 java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Point2D; -import java.util.List; - -import edu.uci.ics.jung.algorithms.layout.Layout; -import edu.uci.ics.jung.graph.Graph; -import edu.uci.ics.jung.graph.util.Context; -import edu.uci.ics.jung.graph.util.Pair; -import edu.uci.ics.jung.visualization.*; -import edu.uci.ics.jung.visualization.renderers.EdgeLabelRenderer; -import edu.uci.ics.jung.visualization.renderers.Renderer; -import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator; -import ghidra.app.plugin.core.functiongraph.graph.FGEdge; -import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; -import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; - -class CodeFlowEdgeLabelRenderer - implements Renderer.EdgeLabel { - - private static final int EDGE_OFFSET = 20; - - VisualGraphVertexShapeTransformer vertexShapeTransformer = - new VisualGraphVertexShapeTransformer(); - - @Override - public void labelEdge(RenderContext rc, Layout layout, E e, String text) { - - if (text == null || text.isEmpty()) { - return; - } - - Graph jungGraph = layout.getGraph(); - Pair endpoints = jungGraph.getEndpoints(e); - V v1 = endpoints.getFirst(); - V v2 = endpoints.getSecond(); - if (!rc.getEdgeIncludePredicate().apply( - Context., E> getInstance(jungGraph, e))) { - return; - } - - if (!rc.getVertexIncludePredicate().apply( - Context., V> getInstance(jungGraph, v1)) || - !rc.getVertexIncludePredicate().apply( - Context., V> getInstance(jungGraph, v2))) { - return; - } - - Point2D p1 = layout.apply(v1); - MultiLayerTransformer multiLayerTransformer = rc.getMultiLayerTransformer(); - p1 = multiLayerTransformer.transform(Layer.LAYOUT, p1); - - Shape vertexShape = vertexShapeTransformer.apply(v1); - Rectangle vertexBounds = vertexShape.getBounds(); - int xDisplacement = rc.getLabelOffset(); - - Point2D labelPointOffset = new Point2D.Double(); - - List articulationPoints = e.getArticulationPoints(); - if (articulationPoints.isEmpty()) { - double vertexBottom = p1.getY() + (vertexBounds.height >> 1); // location is centered - int textY = (int) (vertexBottom + EDGE_OFFSET); // below the vertex; above the bend - int textX = (int) (p1.getX() + xDisplacement); // right of the edge - labelPointOffset.setLocation(textX, textY); - } - else if (articulationPoints.size() == 1) { - // articulation must have been removed - return; - } - else { - - Point2D bend1 = articulationPoints.get(0); - bend1 = multiLayerTransformer.transform(Layer.LAYOUT, bend1); - Point2D bend2 = articulationPoints.get(1); - bend2 = multiLayerTransformer.transform(Layer.LAYOUT, bend2); - - double bx1 = bend1.getX(); - - if (articulationPoints.size() == 2) { - - double vertexSide = p1.getX() + (vertexBounds.width >> 1); // location is centered - int textX = (int) (vertexSide + EDGE_OFFSET); // right of the vertex - int textY = (int) (p1.getY() + EDGE_OFFSET); // above the edge - labelPointOffset.setLocation(textX, textY); - } - else if (articulationPoints.size() == 3) { - double vertexBottom = p1.getY() + (vertexBounds.height >> 1); // location is centered - int textY = (int) (vertexBottom + EDGE_OFFSET); // below the vertex; above the bend - int textX = (int) (bx1 + xDisplacement); // right of the edge - labelPointOffset.setLocation(textX, textY); - } - else if (articulationPoints.size() == 4) { - double vertexBottom = p1.getY() + (vertexBounds.height >> 1); // location is centered - int textY = (int) (vertexBottom + EDGE_OFFSET); // below the vertex; above the bend - int textX = (int) (bx1 + xDisplacement); // right of the edge - labelPointOffset.setLocation(textX, textY); - } - } - EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer(); - Font font = rc.getEdgeFontTransformer().apply(e); - boolean isSelected = rc.getPickedEdgeState().isPicked(e); - Component component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(), - text, font, isSelected, e); - - Dimension d = component.getPreferredSize(); - - GraphicsDecorator g = rc.getGraphicsContext(); - AffineTransform old = g.getTransform(); - AffineTransform xform = new AffineTransform(old); - xform.translate(labelPointOffset.getX(), labelPointOffset.getY()); - - g.setTransform(xform); - g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true); - - g.setTransform(old); - } -} diff --git a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLEdgeLabelRenderer.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLEdgeLabelRenderer.java new file mode 100644 index 0000000000..1675135060 --- /dev/null +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLEdgeLabelRenderer.java @@ -0,0 +1,183 @@ +/* ### + * 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 java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.util.List; + +import com.google.common.base.Predicate; + +import edu.uci.ics.jung.algorithms.layout.Layout; +import edu.uci.ics.jung.graph.Graph; +import edu.uci.ics.jung.graph.util.Context; +import edu.uci.ics.jung.graph.util.Pair; +import edu.uci.ics.jung.visualization.*; +import edu.uci.ics.jung.visualization.renderers.EdgeLabelRenderer; +import edu.uci.ics.jung.visualization.renderers.Renderer; +import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator; +import ghidra.app.plugin.core.functiongraph.graph.FGEdge; +import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; +import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; +import ghidra.program.model.symbol.FlowType; + +/** + * An edge label renderer used with the {@link DecompilerNestedLayout} + * + * @param the vertex type + * @param the edge type + */ +class DNLEdgeLabelRenderer + implements Renderer.EdgeLabel { + + private static final int DEFAULT_EDGE_OFFSET = 20; + + private VisualGraphVertexShapeTransformer vertexShapeTransformer = + new VisualGraphVertexShapeTransformer<>(); + + private double edgeOffset; + + DNLEdgeLabelRenderer(double condenseFactor) { + this.edgeOffset = DEFAULT_EDGE_OFFSET * (1 - condenseFactor); + } + + @Override + public void labelEdge(RenderContext rc, Layout layout, E e, String text) { + + Graph jungGraph = layout.getGraph(); + if (!rc.getEdgeIncludePredicate().apply(Context.getInstance(jungGraph, e))) { + return; + } + + Pair endpoints = jungGraph.getEndpoints(e); + V startv = endpoints.getFirst(); + V endv = endpoints.getSecond(); + + Predicate, V>> includeVertex = rc.getVertexIncludePredicate(); + if (!includeVertex.apply(Context.getInstance(jungGraph, startv)) || + !includeVertex.apply(Context.getInstance(jungGraph, endv))) { + return; + } + + Point2D start = layout.apply(startv); + MultiLayerTransformer multiLayerTransformer = rc.getMultiLayerTransformer(); + start = multiLayerTransformer.transform(Layer.LAYOUT, start); + + Shape vertexShape = vertexShapeTransformer.apply(startv); + Rectangle vertexBounds = vertexShape.getBounds(); + int xDisplacement = rc.getLabelOffset(); + + Point2D labelPointOffset = new Point2D.Double(); + + // note: location is centered + double cx = start.getX(); + double cy = start.getY(); + + EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer(); + Font font = rc.getEdgeFontTransformer().apply(e); + boolean isSelected = rc.getPickedEdgeState().isPicked(e); + Component component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(), + text, font, isSelected, e); + int labelWidth = component.getPreferredSize().width; + + List articulationPoints = e.getArticulationPoints(); + if (articulationPoints.isEmpty()) { + double vertexBottom = start.getY() + (vertexBounds.height >> 1); // location is centered + double textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend + double textX = (int) (start.getX() + xDisplacement); // right of the edge + labelPointOffset.setLocation(textX, textY); + } + else if (articulationPoints.size() == 1) { + // articulation must have been removed + return; + } + else { + + Point2D bend1 = articulationPoints.get(0); + bend1 = multiLayerTransformer.transform(Layer.LAYOUT, bend1); + Point2D bend2 = articulationPoints.get(1); + bend2 = multiLayerTransformer.transform(Layer.LAYOUT, bend2); + + double vertexSide = cx + (vertexBounds.width >> 1); + double vertexBottom = cy + (vertexBounds.height >> 1); + + double bx1 = bend1.getX(); + + FlowType flow = e.getFlowType(); + boolean isRight = flow.isFallthrough() || flow.isUnConditional(); + + if (articulationPoints.size() == 2) { + + double textX = (int) (vertexSide + edgeOffset); // right of the vertex + double textY = (int) (cy + edgeOffset); // above the edge + labelPointOffset.setLocation(textX, textY); + } + else { // 3 or 4 articulations + + double textY = (int) (vertexBottom + edgeOffset); // below the vertex; above the bend + double textX = (int) (bx1 + xDisplacement); // right of the edge + if (!isRight) { + textX = bx1 - xDisplacement - labelWidth; + } + + labelPointOffset.setLocation(textX, textY); + } + } + + Dimension d = component.getPreferredSize(); + + GraphicsDecorator g = rc.getGraphicsContext(); + AffineTransform old = g.getTransform(); + AffineTransform xform = new AffineTransform(old); + xform.translate(labelPointOffset.getX(), labelPointOffset.getY()); + + g.setTransform(xform); + g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true); + g.setTransform(old); + + // debug + //labelArticulations(component, g, rc, e); + } + + @SuppressWarnings("unused") // used during debug + private void labelArticulations(Component component, GraphicsDecorator g, + RenderContext rc, E e) { + + int offset = 5; + int counter = 1; + List points = e.getArticulationPoints(); + for (Point2D p : points) { + + MultiLayerTransformer multiLayerTransformer = rc.getMultiLayerTransformer(); + p = multiLayerTransformer.transform(Layer.LAYOUT, p); + + EdgeLabelRenderer labelRenderer = rc.getEdgeLabelRenderer(); + Font font = rc.getEdgeFontTransformer().apply(e); + boolean isSelected = rc.getPickedEdgeState().isPicked(e); + component = labelRenderer.getEdgeLabelRendererComponent(rc.getScreenDevice(), + "p" + counter++, font, isSelected, e); + + Dimension d = component.getPreferredSize(); + AffineTransform old = g.getTransform(); + AffineTransform xform = new AffineTransform(old); + xform.translate(p.getX() + offset, p.getY()); + g.setTransform(xform); + g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true); + g.setTransform(old); + } + } +} 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..1467a84781 --- /dev/null +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DNLayoutOptions.java @@ -0,0 +1,60 @@ +/* ### + * 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 HELP_ANCHOR = + DecompilerNestedLayoutProvider.LAYOUT_NAME + "_Options"; + 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) { + + HelpLocation help = new HelpLocation(OWNER, HELP_ANCHOR); + + 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; + } + + @Override + public boolean optionChangeRequiresRelayout(String optionName) { + // format: 'Nested Code Layout.Route Edges....' + return optionName.endsWith(USE_EDGE_ROUTING_AROUND_VERTICES_KEY); + } +} 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..39062c64c8 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,52 @@ 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. + * + *

    Edge routing herein defaults to 'simple routing'; 'complex routing' is a user option. + * Simple routing will reduce edge noise as much as possible by combining/overlapping edges that + * flow towards the bottom of the function (returning code flow). Also, edges may fall behind + * vertices for some functions. Complex routing allows the user to visually follow the flow + * of an individual edge. Complex routing will prevent edges from overlapping and will route + * edges around vertices. Simple routing is better when the layout of the vertices is + * important to the user; complex routing is better when edges/relationships are more + * important to the user. + * + * TODO ideas: + * -paint fallthrough differently for all, or just for those returning to the baseline + */ 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(); } @@ -79,7 +108,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override public EdgeLabel getEdgeLabelRenderer() { - return new CodeFlowEdgeLabelRenderer<>(); + return new DNLEdgeLabelRenderer<>(getCondenseFactor()); } @Override @@ -89,6 +118,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 +223,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,135 +251,450 @@ 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); + continue; + } } - 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 multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + if (useSimpleRouting()) { + multiplier = 1; // we allow edges to overlap with 'simple routing' + } + int distanceSpacing = delta * 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)); + + Column column = vertex2dFactory.getColumn(x1); + routeAroundColumnVertices(start, end, column.index, vertex2dFactory, articulations, x2); + + 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)); + + 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 multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + if (useSimpleRouting()) { + multiplier = 1; // we allow edges to overlap with 'simple routing' + } + int distanceSpacing = delta * 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 (usesEdgeArticulations()) { + // 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 multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER; + if (useSimpleRouting()) { + multiplier = 1; // we allow edges to overlap with 'simple routing' + } + int distanceSpacing = delta * 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 (useSimpleRouting()) { + return; + } + + boolean goingDown = true; + int startRow = start.rowIndex; + int endRow = end.rowIndex; + if (startRow > endRow) { // going upwards + goingDown = false; + 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 goingLeft = edgeX < centerX; + + if (!goingDown) { + // for now, any time an edge goes up, we route it to the right + goingLeft = false; + } + + VertexClipper vertexClipper = new VertexClipper(goingLeft, goingDown); + + // no need to check the 'y' value, as the end vertex is above/below this one + if (vertexClipper.isClippingX(otherVertex, edgeX)) { + + /* + + 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 = vertexClipper.getTopOffset(otherVertex, vertexToEdgeOffset); + articulations.add(new Point2D.Double(x, y)); + + // Maybe merge points if they are too close together. Visually, many lines + // moving around intersecting vertices looks busy. When the intersecting + // vertices are close together, we remove some of the articulations in order to + // smooth out the edges. + if (articulations.size() > 2) { + + /* + The last articulation is the one added before this method was called, which + lies just below the intersecting vertex. The articulation before that is + the one that is the one that is sending the x value straight into the + intersecting vertex. Delete that point as well so that the entire edge is + shifted to the outside of the intersecting vertex. This will get repeated + for each vertex that is intersecting. + */ + Point2D previousArticulation = articulations.get(articulations.size() - 2); + int closenessHeight = 50; + double previousY = previousArticulation.getY(); + if (vertexClipper.isTooCloseY(y, previousY, closenessHeight)) { + 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 = vertexClipper.getSideOffset(otherVertex, offset); + articulations.add(new Point2D.Double(x, y)); + + // p3 - same x; move y above/below the vertex + y = vertexClipper.getBottomOffset(otherVertex, 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 boolean useSimpleRouting() { + return !getLayoutOptions().useEdgeRoutingAroundVertices(); + } + + 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 + int startRow = start.rowIndex; + int endRow = end.rowIndex; + if (startRow > endRow) { // going upwards + endRow = start.rowIndex; + startRow = end.rowIndex; + } - Point2D startVertexPoint = vertexLayoutLocations.get(startVertex); + int delta = endRow - startRow; + x += delta; // adding the delta makes overlap less likely + Point2D startVertexPoint = start.center; double y1 = startVertexPoint.getY(); Point2D first = new Point2D.Double(x, y1); articulations.add(first); @@ -344,12 +702,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, @@ -381,7 +747,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { } private void debug(String text) { -// System.err.println(text); + // System.err.println(text); } private void printParts(int depth, BlockGraph block) { @@ -566,13 +932,183 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override protected AbstractVisualGraphLayout createClonedFGLayout( FunctionGraph newGraph) { - return new DecompilerNestedLayout(newGraph, false); + return new DecompilerNestedLayout(newGraph, getLayoutName(), false); } //================================================================================================== // Inner Classes //================================================================================================== + /** + * Encapsulates knowledge of edge direction (up/down, left/right) and uses that knowledge + * to report vertex offsets from the appropriate side and top/bottom + */ + private class VertexClipper { + + boolean goingLeft; + boolean goingDown; + + VertexClipper(boolean isLeft, boolean isBottom) { + this.goingLeft = isLeft; + this.goingDown = isBottom; + } + + private double getSide(Vertex2d v) { + return goingLeft ? v.getLeft() : v.getRight(); + } + + double getTopOffset(Vertex2d v, int offset) { + return goingDown ? v.getTop() - offset : v.getBottom() + offset; + } + + double getBottomOffset(Vertex2d v, int offset) { + return goingDown ? v.getBottom() + offset : v.getTop() - offset; + } + + double getSideOffset(Vertex2d v, int offset) { + + double side = getSide(v); + if (goingLeft) { + return side - offset; + } + return side + offset; + } + + boolean isTooCloseY(double topY, double bottomY, double threshold) { + double delta = goingDown ? topY - bottomY : bottomY - topY; + return delta < threshold; + } + + boolean isClippingX(Vertex2d v, double x) { + + double side = getSide(v); + if (goingLeft) { + return x >= side; + } + return x < side; + } + } + + /** + * Factory for creating and caching {@link Vertex2d} objects + */ + 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 +1202,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { return null; -// return "Block"; TODO could put in a 'debug name' method for debugging } @Override @@ -736,7 +1271,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override public String toString() { - return PcodeBlock.typeToName(pcodeBlock.getType()) + " - " + pcodeBlock.getStart(); + return PcodeBlock.typeToName(pcodeBlock.getType()) + " - " + getName() + " - " + + pcodeBlock.getStart(); } } @@ -834,13 +1370,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 +1410,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { - return "Plain"; // TODO: maybe just null + return "Plain"; } } @@ -902,7 +1438,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout { @Override String getName() { return parent.getName(); -// return "List"; } } @@ -967,7 +1502,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/Docking/src/main/java/docking/menu/MultiStateDockingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java index feec38cda4..468a934ca0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateDockingAction.java @@ -35,6 +35,11 @@ import resources.icons.EmptyIcon; * drop-down icon that allows users to change the state of the button. Also, by default, as * the user presses the button, it will execute the action corresponding to the current * state. + * + *

    Warning: if you use this action in a toolbar, then be sure to call the + * {@link #MultiStateDockingAction(String, String, boolean) correct constructor}. If you call + * another constructor, or pass false for this boolean above, your + * {@link #doActionPerformed(ActionContext)} method will get called twice. * * @param the type of the user data * @see MultiActionDockingAction @@ -56,10 +61,25 @@ public abstract class MultiStateDockingAction extends DockingAction { // stub for toolbar actions }; + /** + * Call this constructor with this action will not be added to a toolbar + * + * @param name the action name + * @param owner the owner + * @see #MultiStateDockingAction(String, String, boolean) + */ public MultiStateDockingAction(String name, String owner) { this(name, owner, false); } + /** + * Use this constructor explicitly when this action is used in a toolbar, passing true + * for isToolbarAction (see the javadoc header note). + * + * @param name the action name + * @param owner the owner + * @param isToolbarAction true if this action is a toolbar action + */ protected MultiStateDockingAction(String name, String owner, boolean isToolbarAction) { super(name, owner); multiActionGenerator = context -> getStateActions(); @@ -87,6 +107,9 @@ public abstract class MultiStateDockingAction extends DockingAction { *

    * Also, if the parameter is true, then the button will behave like a button in terms of * mouse feedback. If false, then the button will behave more like a label. + * + * @param doPerformAction true to call {@link #doActionPerformed(ActionContext)} when the + * user presses the button for this action (not the drop-down menu; see above) */ public void setPerformActionOnPrimaryButtonClick(boolean doPerformAction) { performActionOnPrimaryButtonClick = doPerformAction; @@ -116,6 +139,8 @@ public abstract class MultiStateDockingAction extends DockingAction { * This is the callback to be overridden when the child wishes to respond to user button * presses that are on the button and not the drop-down. This will only be called if * {@link #performActionOnPrimaryButtonClick} is true. + * + * @param context the action context */ protected void doActionPerformed(ActionContext context) { // override me to do work 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" + DockingUtils.CONTROL_KEY_NAME + " key while using the mouse wheel"; - public static final String USE_STICKY_SELECTION = "Use Sticky Selection"; + public static final String USE_STICKY_SELECTION_KEY = "Use Sticky Selection"; public static final String USE_STICKY_SELECTION_DESCRIPTION = "When enabled " + "Selecting code units in one vertex will not clear the selection in another. When " + "disabled, every new selection clears the previous selection unless the Control" + diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphEdgeLabelRenderer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphEdgeLabelRenderer.java new file mode 100644 index 0000000000..d7fd900edb --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphEdgeLabelRenderer.java @@ -0,0 +1,58 @@ +/* ### + * 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.viewer.renderer; + +import java.awt.*; + +import javax.swing.JComponent; + +import edu.uci.ics.jung.visualization.renderers.DefaultEdgeLabelRenderer; + +/** + * Overrides the {@link DefaultEdgeLabelRenderer} so that the client can set the non-picked + * foreground color. See {@link #setNonPickedForegroundColor(Color)}. + */ +public class VisualGraphEdgeLabelRenderer extends DefaultEdgeLabelRenderer { + + private Color nonPickedForegroundColor; + + public VisualGraphEdgeLabelRenderer(Color pickedColor) { + super(pickedColor); + } + + @Override + public Component getEdgeLabelRendererComponent(JComponent vv, Object value, Font font, + boolean isSelected, E edge) { + + super.getEdgeLabelRendererComponent(vv, value, font, isSelected, edge); + + // fixup the parent call to use this label's foreground + if (!isSelected) { + setForeground(nonPickedForegroundColor); + } + + return this; + } + + /** + * Sets the foreground color for this renderer when the edge is not picked/selected + * + * @param color the color + */ + public void setNonPickedForegroundColor(Color color) { + this.nonPickedForegroundColor = color; + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java index 3ced28d8b7..713a186f33 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java @@ -19,6 +19,8 @@ import java.awt.Color; import java.awt.geom.Point2D; import java.util.*; +import com.google.common.base.Function; + import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.visualization.*; @@ -32,6 +34,9 @@ import ghidra.graph.viewer.layout.*; * This was created to add the ability to paint selected vertices above other vertices. We need * this since the Jung Graph has no notion of Z-order and thus does not let us specify that any * particular vertex should be above another one. + * + * @param the vertex type + * @param the edge type */ public class VisualGraphRenderer> extends edu.uci.ics.jung.visualization.renderers.BasicRenderer { @@ -83,7 +88,6 @@ public class VisualGraphRenderer // 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); // } @@ -109,11 +113,13 @@ public class VisualGraphRenderer return; } - String label = rc.getEdgeLabelTransformer().apply(e); + Function xform = rc.getEdgeLabelTransformer(); + String label = xform.apply(e); if (label == null) { return; } - super.renderEdgeLabel(rc, layout, e); + + edgeLabelRenderer.labelEdge(rc, layout, e, xform.apply(e)); } private void paintLayoutGridCells(RenderContext renderContext, Layout 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")