From f54bd20d40f50b95c3dd8309d79bb88415d501ad Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:20:47 -0400 Subject: [PATCH] GP-5481 Created prototype data graph feature --- Ghidra/Debug/Debugger/build.gradle | 1 + .../graph/data/DebuggerDataGraphPlugin.java | 76 ++ .../CircleWithLabelVertexShapeProvider.java | 8 +- .../program/database/ProgramBuilder.java | 44 + Ghidra/Features/DataGraph/Module.manifest | 1 + Ghidra/Features/DataGraph/build.gradle | 34 + .../Features/DataGraph/certification.manifest | 8 + .../DataGraph/data/datagraph.theme.properties | 4 + .../src/main/help/help/TOC_Source.xml | 64 ++ .../topics/DataGraphPlugin/Data_Graph.html | 250 ++++++ .../DataGraphPlugin/images/CodeVertex.png | Bin 0 -> 20126 bytes .../DataGraphPlugin/images/DataGraph.png | Bin 0 -> 49444 bytes .../datagraph/AbstractDataGraphPlugin.java | 84 ++ .../main/java/datagraph/DataGraphPlugin.java | 75 ++ .../java/datagraph/DataGraphProvider.java | 296 +++++++ .../src/main/java/datagraph/DegContext.java | 54 ++ .../java/datagraph/DegSatelliteContext.java | 32 + .../datagraph/data/graph/CodeDegVertex.java | 181 +++++ .../datagraph/data/graph/DataDegVertex.java | 390 +++++++++ .../data/graph/DataExplorationGraph.java | 61 ++ .../datagraph/data/graph/DegController.java | 749 ++++++++++++++++++ .../java/datagraph/data/graph/DegEdge.java | 35 + .../datagraph/data/graph/DegGraphView.java | 60 ++ .../java/datagraph/data/graph/DegLayout.java | 71 ++ .../data/graph/DegLayoutProvider.java | 46 ++ .../java/datagraph/data/graph/DegVertex.java | 116 +++ .../data/graph/panel/DataVertexPanel.java | 656 +++++++++++++++ .../panel/DtComponentPathComparator.java | 43 + .../model/column/CompactDataColumnModel.java | 132 +++ .../model/column/ExpandedDataColumnModel.java | 122 +++ .../model/column/PointerColumnRenderer.java | 44 + .../model/column/ValueColumnRenderer.java | 41 + .../model/row/ArrayGroupDataRowObject.java | 61 ++ .../model/row/ComponentDataRowObject.java | 65 ++ .../graph/panel/model/row/DataRowObject.java | 85 ++ .../panel/model/row/DataRowObjectCache.java | 59 ++ .../panel/model/row/DataTrableRowModel.java | 141 ++++ .../panel/model/row/OpenDataChildren.java | 469 +++++++++++ .../explore/AbstractExplorationGraph.java | 143 ++++ .../java/datagraph/graph/explore/EgEdge.java | 30 + .../graph/explore/EgEdgeRenderer.java | 49 ++ .../graph/explore/EgEdgeTransformer.java | 88 ++ .../graph/explore/EgGraphLayout.java | 268 +++++++ .../datagraph/graph/explore/EgVertex.java | 129 +++ .../graph/explore/GraphLocationMap.java | 130 +++ .../resources/images/view_detailed_16.png | Bin 0 -> 961 bytes .../graph/DataGraphProviderTest.java | 531 +++++++++++++ .../datagraph/graph/EgGraphLayoutTest.java | 304 +++++++ .../datagraph/graph/GraphLocationMapTest.java | 91 +++ .../src/main/help/help/TOC_Source.xml | 2 +- .../FunctionGraphPlugin/Function_Graph.html | 157 +--- .../functiongraph/graph/FunctionGraph.java | 2 +- .../graph/layout/EmptyLayout.java | 8 +- .../flowchart/AbstractFlowChartLayout.java | 2 +- .../layout/flowchart/FlowChartLayoutTest.java | 4 +- .../layout/flowchart/EdgeSegmentTest.java | 2 - .../graph/FcgVertexShapeProvider.java | 2 +- .../main/java/docking/DockableComponent.java | 1 + .../java/docking/DockableToolBarManager.java | 3 +- .../src/main/java/docking/GenericHeader.java | 12 +- .../tabbedpane/DockingTabRenderer.java | 5 +- .../trable/AbstractGTrableRowModel.java | 45 ++ .../trable/DefaultGTrableCellRenderer.java | 51 ++ .../trable/DefaultGTrableRowModel.java | 98 +++ .../java/docking/widgets/trable/GTrable.java | 646 +++++++++++++++ .../trable/GTrableCellClickedListener.java | 32 + .../widgets/trable/GTrableCellRenderer.java | 40 + .../docking/widgets/trable/GTrableColumn.java | 76 ++ .../widgets/trable/GTrableColumnModel.java | 252 ++++++ .../trable/GTrableModeRowlListener.java | 27 + .../docking/widgets/trable/GTrableRow.java | 66 ++ .../widgets/trable/GTrableRowModel.java | 84 ++ .../docking/widgets/trable/OpenCloseIcon.java | 105 +++ .../docking/widgets/trable/GTrableTest.java | 338 ++++++++ Ghidra/Framework/Graph/build.gradle | 5 +- Ghidra/Framework/Graph/certification.manifest | 2 + .../Graph/src/main/help/help/TOC_Source.xml | 54 ++ .../help/topics/VisualGraph/Visual_Graph.html | 149 ++++ .../graph/VisualGraphComponentProvider.java | 12 +- .../featurette/VgSatelliteFeaturette.java | 53 +- .../job/RelayoutAndCenterVertexGraphJob.java | 78 ++ .../graph/job/RelayoutAndEnsureVisible.java | 166 ++++ .../graph/job/RelayoutFunctionGraphJob.java | 27 +- .../ghidra/graph/viewer/GraphComponent.java | 19 +- .../ghidra/graph/viewer/GraphViewerUtils.java | 44 +- .../viewer/event/mouse/VertexMouseInfo.java | 36 +- ...lGraphEventForwardingGraphMousePlugin.java | 5 +- .../VisualGraphPickingGraphMousePlugin.java | 25 +- .../mouse/VisualGraphPluggableGraphMouse.java | 110 ++- .../layout/AbstractVisualGraphLayout.java | 10 +- .../graph/viewer/layout/GridBounds.java | 10 + .../graph/viewer/layout/GridLocationMap.java | 49 +- .../ghidra/graph/viewer/layout/GridPoint.java | 6 + .../JungWrappingVisualGraphLayoutAdapter.java | 10 +- .../graph/viewer/layout/LayoutListener.java | 13 +- .../graph/viewer/layout/LayoutPositions.java | 9 +- .../viewer/layout/VisualGraphLayout.java | 12 +- .../shape/ArticulatedEdgeTransformer.java | 81 +- .../shape/VisualGraphShapePickSupport.java | 5 +- .../main/java/generic/theme}/CloseIcon.java | 11 +- .../Gui/src/main/java/resources/Icons.java | 6 +- .../DataGraphPluginScreenShots.java | 145 ++++ 102 files changed, 9267 insertions(+), 366 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/graph/data/DebuggerDataGraphPlugin.java create mode 100644 Ghidra/Features/DataGraph/Module.manifest create mode 100644 Ghidra/Features/DataGraph/build.gradle create mode 100644 Ghidra/Features/DataGraph/certification.manifest create mode 100644 Ghidra/Features/DataGraph/data/datagraph.theme.properties create mode 100644 Ghidra/Features/DataGraph/src/main/help/help/TOC_Source.xml create mode 100644 Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/Data_Graph.html create mode 100644 Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/CodeVertex.png create mode 100644 Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/DataGraph.png create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/AbstractDataGraphPlugin.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphPlugin.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphProvider.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/DegContext.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/DegSatelliteContext.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/CodeDegVertex.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataDegVertex.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataExplorationGraph.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegController.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegEdge.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegGraphView.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayout.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayoutProvider.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegVertex.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DataVertexPanel.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DtComponentPathComparator.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/CompactDataColumnModel.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ExpandedDataColumnModel.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/PointerColumnRenderer.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ValueColumnRenderer.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ArrayGroupDataRowObject.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ComponentDataRowObject.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObject.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObjectCache.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataTrableRowModel.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/OpenDataChildren.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/AbstractExplorationGraph.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdge.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeRenderer.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeTransformer.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgGraphLayout.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgVertex.java create mode 100644 Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/GraphLocationMap.java create mode 100644 Ghidra/Features/DataGraph/src/main/resources/images/view_detailed_16.png create mode 100644 Ghidra/Features/DataGraph/src/test/java/datagraph/graph/DataGraphProviderTest.java create mode 100644 Ghidra/Features/DataGraph/src/test/java/datagraph/graph/EgGraphLayoutTest.java create mode 100644 Ghidra/Features/DataGraph/src/test/java/datagraph/graph/GraphLocationMapTest.java rename Ghidra/Features/FunctionGraph/src/test/{java => }/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java (99%) create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/AbstractGTrableRowModel.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableCellRenderer.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableRowModel.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrable.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellClickedListener.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellRenderer.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumn.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumnModel.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableModeRowlListener.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRow.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRowModel.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/OpenCloseIcon.java create mode 100644 Ghidra/Framework/Docking/src/test/java/docking/widgets/trable/GTrableTest.java create mode 100644 Ghidra/Framework/Graph/src/main/help/help/TOC_Source.xml create mode 100644 Ghidra/Framework/Graph/src/main/help/help/topics/VisualGraph/Visual_Graph.html create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutAndCenterVertexGraphJob.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutAndEnsureVisible.java rename Ghidra/Framework/{Docking/src/main/java/docking => Gui/src/main/java/generic/theme}/CloseIcon.java (95%) create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataGraphPluginScreenShots.java diff --git a/Ghidra/Debug/Debugger/build.gradle b/Ghidra/Debug/Debugger/build.gradle index 5bb9a15cbc..2cd52327a8 100644 --- a/Ghidra/Debug/Debugger/build.gradle +++ b/Ghidra/Debug/Debugger/build.gradle @@ -31,6 +31,7 @@ dependencies { api project(':DecompilerDependent') api project(':FunctionGraph') api project(':ProposedUtils') + api project(':DataGraph') testImplementation project(path: ':Generic', configuration: 'testArtifacts') testImplementation project(path: ':Base', configuration: 'testArtifacts') diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/graph/data/DebuggerDataGraphPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/graph/data/DebuggerDataGraphPlugin.java new file mode 100644 index 0000000000..e1846e08be --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/graph/data/DebuggerDataGraphPlugin.java @@ -0,0 +1,76 @@ +/* ### + * 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.debug.gui.graph.data; + +import datagraph.AbstractDataGraphPlugin; +import ghidra.app.context.ListingActionContext; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.event.TraceLocationPluginEvent; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.util.ProgramLocation; + +/** + * Plugin for showing a graph of data from the listing. + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = DebuggerPluginPackage.NAME, + category = PluginCategoryNames.DEBUGGER, + shortDescription = "Debugger Data Graph", + description = """ + Plugin for displaying graphs of data objects in memory. From any data object in the + listing, the user can display a graph of that data object. Initially, a graph will be shown + with one vertex that has a scrollable view of the values in memory associated with that data. + Also, any pointers or references from or to that data can be explored by following the + references and creating additional vertices for the referenced code or data. + """, + eventsConsumed = { + TraceLocationPluginEvent.class, + }, + eventsProduced = { + TraceLocationPluginEvent.class, + } +) +//@formatter:on +public class DebuggerDataGraphPlugin extends AbstractDataGraphPlugin { + public DebuggerDataGraphPlugin(PluginTool plugintool) { + super(plugintool); + } + + @Override + public void processEvent(PluginEvent event) { + if (event instanceof TraceLocationPluginEvent ev) { + ProgramLocation location = ev.getLocation(); + goTo(location); + } + } + + @Override + public void fireLocationEvent(ProgramLocation location) { + firePluginEvent(new TraceLocationPluginEvent(getName(), location)); + } + + @Override + protected boolean isGraphActionEnabled(ListingActionContext context) { + if (!context.getNavigatable().isDynamic()) { + return false; + } + return super.isGraphActionEnabled(context); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/base/graph/CircleWithLabelVertexShapeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/base/graph/CircleWithLabelVertexShapeProvider.java index 743f039f7d..9a042bf216 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/base/graph/CircleWithLabelVertexShapeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/base/graph/CircleWithLabelVertexShapeProvider.java @@ -73,6 +73,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider { protected boolean useDebugBorders = false; private String fullLabelText; + private int circleCenterYOffset; public CircleWithLabelVertexShapeProvider(String label) { this.fullLabelText = label; @@ -85,6 +86,10 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider { buildUi(); } + public int getCircleCenterYOffset() { + return circleCenterYOffset; + } + protected void buildUi() { String name = generateLabelText(); @@ -201,6 +206,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider { vertexImageLabel.setBounds(x, y, size.width, size.height); Dimension shapeSize = vertexShape.getBounds().getSize(); + circleCenterYOffset = shapeSize.height / 2 - parentSize.height / 2; // setFrame() will make sure the shape's x,y values are where they need to be // for the later 'full shape' creation @@ -332,7 +338,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider { return false; } - protected void setTogglesVisible(boolean visible) { + public void setTogglesVisible(boolean visible) { toggleInsButton.setVisible(visible); toggleOutsButton.setVisible(visible); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java index 13fda9b7cd..23add48ab5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java @@ -397,6 +397,50 @@ public class ProgramBuilder { } } + public void setString(String address, String string) throws Exception { + byte[] bytes = string.getBytes(); + setBytes(address, bytes); + } + + public void setShort(String address, short value) throws Exception { + DataConverter converter = getDataConverter(); + byte[] bytes = converter.getBytes(value); + setBytes(address, bytes); + } + + public void setInt(String address, int value) throws Exception { + DataConverter converter = getDataConverter(); + byte[] bytes = converter.getBytes(value); + setBytes(address, bytes); + } + + public void setLong(String address, long value) throws Exception { + DataConverter converter = getDataConverter(); + byte[] bytes = converter.getBytes(value); + setBytes(address, bytes); + } + + public void putAddress(String address, String pointerAddress) throws Exception { + Address pointer = addr(pointerAddress); + long offset = pointer.getOffset(); + int pointerSize = pointer.getAddressSpace().getPointerSize(); + switch (pointerSize) { + case 2: + setShort(address, (short) offset); + break; + case 4: + setInt(address, (int) offset); + break; + default: + setLong(address, offset); + } + } + + private DataConverter getDataConverter() { + boolean bigEndian = program.getMemory().isBigEndian(); + return bigEndian ? BigEndianDataConverter.INSTANCE : LittleEndianDataConverter.INSTANCE; + } + public void setRead(MemoryBlock block, boolean r) { tx(() -> block.setRead(r)); } diff --git a/Ghidra/Features/DataGraph/Module.manifest b/Ghidra/Features/DataGraph/Module.manifest new file mode 100644 index 0000000000..5f6a55051e --- /dev/null +++ b/Ghidra/Features/DataGraph/Module.manifest @@ -0,0 +1 @@ +EXCLUDE FROM GHIDRA JAR: true diff --git a/Ghidra/Features/DataGraph/build.gradle b/Ghidra/Features/DataGraph/build.gradle new file mode 100644 index 0000000000..1ecb5f3f26 --- /dev/null +++ b/Ghidra/Features/DataGraph/build.gradle @@ -0,0 +1,34 @@ +/* ### + * 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. + */ +apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle" +apply from: "$rootProject.projectDir/gradle/javaProject.gradle" +apply from: "$rootProject.projectDir/gradle/helpProject.gradle" +apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle" +apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle" +apply plugin: 'eclipse' + +eclipse.project.name = 'Features Data Graph' + + +// Note: this module's name is 'Data Graph' +dependencies { + api project(":Base") + + // These have abstract test classes and stubs needed by this module + testImplementation project(path: ':Project', configuration: 'testArtifacts') + testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts') + +} diff --git a/Ghidra/Features/DataGraph/certification.manifest b/Ghidra/Features/DataGraph/certification.manifest new file mode 100644 index 0000000000..4a79ed6350 --- /dev/null +++ b/Ghidra/Features/DataGraph/certification.manifest @@ -0,0 +1,8 @@ +##VERSION: 2.0 +Module.manifest||GHIDRA||||END| +data/datagraph.theme.properties||GHIDRA||||END| +src/main/help/help/TOC_Source.xml||GHIDRA||||END| +src/main/help/help/topics/DataGraphPlugin/Data_Graph.html||GHIDRA||||END| +src/main/help/help/topics/DataGraphPlugin/images/CodeVertex.png||GHIDRA||||END| +src/main/help/help/topics/DataGraphPlugin/images/DataGraph.png||GHIDRA||||END| +src/main/resources/images/view_detailed_16.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| diff --git a/Ghidra/Features/DataGraph/data/datagraph.theme.properties b/Ghidra/Features/DataGraph/data/datagraph.theme.properties new file mode 100644 index 0000000000..6278ada3f1 --- /dev/null +++ b/Ghidra/Features/DataGraph/data/datagraph.theme.properties @@ -0,0 +1,4 @@ +[Defaults] +color.fg.datagraph.value = color.palette.blue +icon.plugin.datagraph.action.viewer.vertex.format = view_detailed_16.png +icon.plugin.datagraph.action.viewer.reset = icon.refresh diff --git a/Ghidra/Features/DataGraph/src/main/help/help/TOC_Source.xml b/Ghidra/Features/DataGraph/src/main/help/help/TOC_Source.xml new file mode 100644 index 0000000000..ba6629ee26 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/help/help/TOC_Source.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + diff --git a/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/Data_Graph.html b/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/Data_Graph.html new file mode 100644 index 0000000000..979f025f00 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/Data_Graph.html @@ -0,0 +1,250 @@ + + + + + + + Function Graph Plugin + + + + + +

Data Graph

+ + + + + + + +
+ +
+

The data graph is a graph display that shows data objects in memory. Each data vertex + displays a scrollable view of a data object and its contents.

+ +

Initially, a data graph is generated from a specific data program location. The graph + will be populated with one data vertex showing the data for that location. From this original + source vertex, the references to and from this vertex can be explored to add additional + vertices to the graph. In the data vertex display, any data elements that have an outgoing + reference will have an icon that can be clicked + to quickly explore that reference.

+ +

If a reference leads from/to code, a simple code vertex is generated + that simply shows the function and offset of the reference from the function's entry point. + For the purposes of the data graph, code vertices are end points and cannot be explored + further.

+ +

The display consists of the Primary View and an optional Satellite View.

+
+ +

Primary View

+ +
+

The primary view shows an initial data object that was used to create the graph. This + data vertex can be used to explore references and pointers. As you explore, new vertices + will be added to the graph. All vertices in the graph can trace a reference relationship + back to the original source data object.

+ +
+

The original source data vertex + has a icon in its header to indicate it is the original source vertex.

+
+
+ +

Vertices

+
+

Data Vertices

+ +
+

Data vertices show information about the data and values they contain. If the vertex + datatype is a primitive, such as an int, the vertex contains only one row that shows its name + and value. If the data is more complex such as a structure or array, the vertex will display + multiple rows showing the name and value of each field in the structure. If the structure + contains other structures or arrays of data, those data items can be expanded in a tree/table + structure showing the names and values of that internal data.

+ +

Any elements that are pointers (or have attached outgoing references) will display a + icon that can be clicked to explore those + references to add new vertices.

+ +

Data vertices can be resized by dragging the bottom right corner. Also, the interior column + sizes can be adjusted by hovering the mouse on a column boundary and dragging left or + right.

+ +

The header of a vertex contains the name of the label and/or address of the data object. + The header also contains buttons that allow you to perform some common operations on the + vertex.

+ +

As long as you are within the interaction threshold, + you may interact with the vertex to expand/collapse sub-data elements and to click on any + pointer references to add vertices to the graph.

+ +
+

Code Vertices

+
+

Generally vertices in the graph show data objects, but a vertex can also represent a + reference from or to code. In the data graph, code vertices are terminal vertices, simply + showing the function name and offset into or out of that function.

+ + + + + + + +
+ +

In the image above, the graph is showing two references to the same string.

+
+ +

Vertex Layout

+
+

The data graph uses a special layout to attempt to maintain a logical structure as + more vertices are added to the graph. This layout uses the exploration order + to layout the vertices.

+ +

Starting with the original data vertex. All vertices that were + added to the graph by following outgoing references are displayed in a column to the right of + the vertex. Within this column, the vertices are ordered by the order they are first referenced + from the source vertex.

+ +

Similarly, vertices added to the graph by following incoming references are shown in a + column to the left of the source vertex. Within this column, the vertices are ordered by + address.

+ +

Additional layers of vertices can be added by further exploring the child vertices and their + descendants. These additional vertices are ordered in the same way relative to their immediate + source vertex.

+ +

Whenever a vertex is removed from the graph, all vertices that were discovered by exploring + from that vertex are also removed.

+ +
+

Vertex Actions

+ +
+

The following toolbar actions are available on a data vertex.

+ + + +

The following popup actions are are available depending on where the + mouse is positioned when the popup is triggered.

+ + +
+

Selecting Vertices

+ +
+

Left-clicking a vertex will select that vertex. To select multiple vertices, hold down + the Ctrl key while clicking. To deselect + a block, hold the Ctrl key while clicking the block. To clear all selected + blocks, click in an empty area of the primary view. When selected, a block is adorned + with a halo.

+ +

You may also select multiple vertices in one action by holding the Ctrl key + while performing a drag operation. Press the Ctrl key and start the drag in an + empty area of the primary view (not over a vertex). This will create a bounding rectangle + on the screen that will select any vertices contained therein when the action is + finished.

+
+ +

Navigating Vertices

+ +
+

By default the data graph can be used to navigate the main views of the tool. Clicking + on a vertex or a row within a vertex will generate a goto event for that address, causing + the main tool to navigate to that location. See the actions below to turn off this + behavior.

+ +

Also, the data graph can listen for tool location changes and select the appropriate + vertex in the graph if any vertices contain the tool location address. This behavior is + off by default, but can be turn on via a popup action.

+
+
+ +

Data Graph Actions

+
+

Toolbar Actions

+ + +
Standard Graph Features and Actions +
+

The data graph is a type of Ghidra Visual Graph and has some standard concepts, features + and actions. +

+ +
+
+ +

Provided by: Data Graph Plugin

+
+
+ + diff --git a/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/CodeVertex.png b/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/CodeVertex.png new file mode 100644 index 0000000000000000000000000000000000000000..9f6c8122ae6ecb5967a18720aaa86ec4a9b2b15b GIT binary patch literal 20126 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 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/DataGraph.png b/Ghidra/Features/DataGraph/src/main/help/help/topics/DataGraphPlugin/images/DataGraph.png new file mode 100644 index 0000000000000000000000000000000000000000..93020d9c2e4ed1b20ab3a90295c81271551ae298 GIT binary patch literal 49444 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%0AKe5X=e|QK}j-|JdbY)TIR0Y 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 activeProviders = new HashSet<>(); + + public AbstractDataGraphPlugin(PluginTool plugintool) { + super(plugintool); + createActions(); + } + + public void goTo(ProgramLocation location) { + activeProviders.forEach(p -> p.goTo(location)); + } + + private void createActions() { + + new ActionBuilder("Display Data Graph", getName()) + .popupMenuPath("Data", "Display Data Graph") + .keyBinding("ctrl G") + .helpLocation(new HelpLocation("DataGraphPlugin", "Data_Graph")) + .withContext(ListingActionContext.class) + .enabledWhen(this::isGraphActionEnabled) + .onAction(this::showDataGraph) + .buildAndInstall(tool); + } + + protected boolean isGraphActionEnabled(ListingActionContext context) { + return context.getCodeUnit() instanceof Data; + } + + private void showDataGraph(ListingActionContext context) { + Data data = (Data) context.getCodeUnit(); + // the data from the context may be an internal sub-data, we want the outermost data. + data = getTopLevelData(data); + DataGraphProvider provider = new DataGraphProvider(this, data); + activeProviders.add(provider); + tool.showComponentProvider(provider, true); + } + + private Data getTopLevelData(Data data) { + Data parent = data.getParent(); + while (parent != null) { + data = parent; + parent = data.getParent(); + } + return data; + } + + void removeProvider(DataGraphProvider provider) { + activeProviders.remove(provider); + } + + public abstract void fireLocationEvent(ProgramLocation location); +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphPlugin.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphPlugin.java new file mode 100644 index 0000000000..229daf7e6d --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphPlugin.java @@ -0,0 +1,75 @@ +/* ### + * 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 datagraph; + +import ghidra.app.CorePluginPackage; +import ghidra.app.context.ListingActionContext; +import ghidra.app.events.ProgramLocationPluginEvent; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.util.ProgramLocation; + +/** + * Plugin for showing a graph of data from the listing. + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.GRAPH, + shortDescription = "Data Graph", + description = """ + Plugin for displaying graphs of data objects in memory. From any data object in the + listing, the user can display a graph of that data object. Initially, a graph will be shown + with one vertex that has a scrollable view of the values in memory associated with that data. + Also, any pointers or references from or to that data can be explored by following the + references and creating additional vertices for the referenced code or data. + """, + eventsConsumed = { + ProgramLocationPluginEvent.class, + }, + eventsProduced = { + ProgramLocationPluginEvent.class, + } +) +//@formatter:on +public class DataGraphPlugin extends AbstractDataGraphPlugin { + public DataGraphPlugin(PluginTool plugintool) { + super(plugintool); + } + + @Override + public void processEvent(PluginEvent event) { + if (event instanceof ProgramLocationPluginEvent ev) { + ProgramLocation location = ev.getLocation(); + goTo(location); + } + } + + @Override + public void fireLocationEvent(ProgramLocation location) { + firePluginEvent(new ProgramLocationPluginEvent(getName(), location, location.getProgram())); + } + + @Override + protected boolean isGraphActionEnabled(ListingActionContext context) { + if (context.getNavigatable().isDynamic()) { + return false; + } + return super.isGraphActionEnabled(context); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphProvider.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphProvider.java new file mode 100644 index 0000000000..7f7491103e --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/DataGraphProvider.java @@ -0,0 +1,296 @@ +/* ### + * 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 datagraph; + +import java.awt.BorderLayout; +import java.awt.event.MouseEvent; +import java.util.Set; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import datagraph.data.graph.*; +import docking.ActionContext; +import docking.ComponentProvider; +import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; +import generic.theme.GIcon; +import ghidra.graph.VisualGraphComponentProvider; +import ghidra.graph.viewer.*; +import ghidra.graph.viewer.GraphComponent.SatellitePosition; +import ghidra.graph.viewer.event.mouse.VertexMouseInfo; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.util.HelpLocation; +import ghidra.util.exception.AssertException; +import resources.Icons; + +/** + * A {@link ComponentProvider} that is the UI component of the {@link DataGraphPlugin}. This + * shows a graph of a Data object in memory and its referenced objects. + */ +public class DataGraphProvider + extends VisualGraphComponentProvider { + + private static final GIcon DETAILS_ICON = + new GIcon("icon.plugin.datagraph.action.viewer.vertex.format"); + private static final GIcon RESET_ICON = new GIcon("icon.plugin.datagraph.action.viewer.reset"); + private static final String NAME = "Data Graph"; + + private AbstractDataGraphPlugin plugin; + private JPanel mainPanel; + + private DegController controller; + private ToggleDockingAction navagateInAction; + private ToggleDockingAction navagateOutAction; + private ToggleDockingAction expandedFormatAction; + + /** + * Constructor + * @param plugin the DataGraphPlugin + * @param data the initial data object to display in the graph. + */ + public DataGraphProvider(AbstractDataGraphPlugin plugin, Data data) { + super(plugin.getTool(), NAME, plugin.getName()); + this.plugin = plugin; + controller = new DegController(this, data); + createActions(); + setTransient(); + + buildComponent(); + addToTool(); + addSatelliteFeature(false, SatellitePosition.LOWER_LEFT); + + setHelpLocation(new HelpLocation("DataGraphPlugin", "DataGraphPlugin")); + } + + private void buildComponent() { + mainPanel = new JPanel(new BorderLayout()); + mainPanel.add(controller.getComponent()); + } + + @Override + public VisualGraphView getView() { + return controller.getView(); + } + + @Override + public ActionContext getActionContext(MouseEvent event) { + Set selectedVertices = getSelectedVertices(); + if (event == null) { + return new DegContext(this, controller.getFocusedVertex(), + selectedVertices); + } + Object source = event.getSource(); + if (source instanceof SatelliteGraphViewer) { + return new DegSatelliteContext(this); + } + + if (source instanceof GraphViewer) { + @SuppressWarnings("unchecked") + GraphViewer viewer = (GraphViewer) source; + + VertexMouseInfo vertexMouseInfo = + GraphViewerUtils.convertMouseEventToVertexMouseEvent(viewer, event); + DegVertex target = vertexMouseInfo != null ? vertexMouseInfo.getVertex() : null; + return new DegContext(this, target, selectedVertices, vertexMouseInfo); + } + throw new AssertException( + "Received mouse event from unexpected source in getActionContext(): " + source); + } + + @Override + public void dispose() { + plugin.removeProvider(this); + controller.dispose(); + super.dispose(); + removeFromTool(); + } + + @Override + public JComponent getComponent() { + return mainPanel; + } + + @Override + public void closeComponent() { + super.closeComponent(); + dispose(); + } + + public Program getProgram() { + return controller.getProgram(); + } + + public DegController getController() { + return controller; + } + + private void createActions() { + new ActionBuilder("Select Home Vertex", plugin.getName()) + .toolBarIcon(Icons.HOME_ICON) + .toolBarGroup("A") + .description("Selects and Centers Original Source Vertx") + .onAction(c -> controller.selectAndCenterHomeVertex()) + .buildAndInstallLocal(this); + + new ActionBuilder("Relayout Graph", plugin.getName()) + .toolBarIcon(RESET_ICON) + .toolBarGroup("A") + .description("Erases all manual vertex positioning information") + .onAction(c -> controller.resetAndRelayoutGraph()) + .buildAndInstallLocal(this); + expandedFormatAction = new ToggleActionBuilder("Show Expanded Format", plugin.getName()) + .toolBarIcon(DETAILS_ICON) + .toolBarGroup("A") + .description("Show Expanded information in data vertices.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Expanded_Format")) + .onAction(c -> controller.setCompactFormat(!expandedFormatAction.isSelected())) + .buildAndInstallLocal(this); + + navagateInAction = + new ToggleActionBuilder("Navigate on Incoming Location Changes", plugin.getName()) + .sharedKeyBinding() + .toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON) + .toolBarGroup("B") + .description("Attemps to select vertex corresponding to tool location changes.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Navigate_In")) + .onAction(c -> controller.setNavigateIn(navagateInAction.isSelected())) + .buildAndInstallLocal(this); + + // this name is same as SelectionNavigationAction which allows sharing of keybinding + navagateOutAction = new ToggleActionBuilder("Selection Navigation Action", plugin.getName()) + .toolBarIcon(Icons.NAVIGATE_ON_OUTGOING_EVENT_ICON) + .toolBarGroup("B") + .sharedKeyBinding() + .description( + "Selecting vetices or locations inside a vertex sends navigates the tool.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Navigate_Out")) + .onAction(c -> controller.setNavigateOut(navagateOutAction.isSelected())) + .selected(true) + .buildAndInstallLocal(this); + + new ActionBuilder("Incoming References", plugin.getName()) + .popupMenuPath("Add All Incoming References") + .popupMenuGroup("A", "2") + .description("Show Vertices for known references to this vertex.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Add Incoming")) + .withContext(DegContext.class) + .enabledWhen(c -> canShowReferences(c.getVertex())) + .onAction(c -> controller.showAllIncommingReferences((DataDegVertex) c.getVertex())) + .buildAndInstallLocal(this); + + new ActionBuilder("Outgoing References", plugin.getName()) + .popupMenuPath("Add All Outgoing References") + .popupMenuGroup("A", "1") + .description("Show Vertices for known references to this vertex.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Add Outgoing")) + .withContext(DegContext.class) + .enabledWhen(c -> canShowReferences(c.getVertex())) + .onAction(c -> controller.showAllOutgoingReferences((DataDegVertex) c.getVertex())) + .buildAndInstallLocal(this); + + new ActionBuilder("Delete Vertices", plugin.getName()) + .popupMenuPath("Delete Selected Vertices") + .popupMenuGroup("B", "1") + .description("Removes the selected vertices and their descendents from the graph") + .helpLocation(new HelpLocation("DataGraphPlugin", "Delete_Selected")) + .withContext(DegContext.class) + .enabledWhen(c -> canClose(c.getSelectedVertices())) + .onAction(c -> controller.deleteVertices(c.getSelectedVertices())) + .buildAndInstallLocal(this); + + new ActionBuilder("Set Original Vertex", plugin.getName()) + .popupMenuPath("Set Vertex as Original Source") + .popupMenuGroup("B", "2") + .description("Reorient graph as though this was the first vertex shown") + .helpLocation(new HelpLocation("DataGraphPlugin", "Original_Source")) + .withContext(DegContext.class) + .enabledWhen(c -> canOrientGraphAround(c.getVertex())) + .onAction(c -> controller.orientAround(c.getVertex())) + .buildAndInstallLocal(this); + + new ActionBuilder("Reset Vertex Location", plugin.getName()) + .popupMenuPath("Restore Location") + .popupMenuGroup("B", "3") + .popupMenuIcon(Icons.REFRESH_ICON) + .description("Resets the vertex to the automated layout location.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Reset_Location")) + .withContext(DegContext.class) + .enabledWhen(c -> c.getVertex() != null && c.getVertex().hasUserChangedLocation()) + .onAction(c -> c.getVertex().clearUserChangedLocation()) + .buildAndInstallLocal(this); + + new ActionBuilder("Expand Fully", plugin.getName()) + .popupMenuPath("Expand Fully") + .popupMenuGroup("C", "1") + .description("Expand all levels under selected row") + .helpLocation(new HelpLocation("DataGraphPlugin", "Expand_Fully")) + .withContext(DegContext.class) + .enabledWhen(this::canExpandRecursively) + .onAction(this::expandRecursively) + .buildAndInstallLocal(this); + } + + private boolean canOrientGraphAround(DegVertex vertex) { + if (vertex instanceof DataDegVertex) { + return !vertex.isRoot(); + } + return false; + } + + private boolean canShowReferences(DegVertex vertex) { + return vertex instanceof DataDegVertex; + } + + private boolean canClose(Set selectedVertices) { + if (selectedVertices.isEmpty()) { + return false; + } + if (selectedVertices.size() > 1) { + return true; + } + + // Special case for just one vertex selected. Can't delete the root vertex. + DegVertex v = selectedVertices.iterator().next(); + return !v.isRoot(); + } + + void goTo(ProgramLocation location) { + controller.locationChanged(location); + } + + private boolean canExpandRecursively(DegContext context) { + DegVertex vertex = context.getVertex(); + if (vertex instanceof DataDegVertex dataVertex) { + return dataVertex.isOnExpandableRow(); + } + return false; + } + + private void expandRecursively(DegContext context) { + DataDegVertex vertex = (DataDegVertex) context.getVertex(); + vertex.expandSelectedRowRecursively(); + } + + public void navigateOut(ProgramLocation location) { + plugin.fireLocationEvent(location); + + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/DegContext.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/DegContext.java new file mode 100644 index 0000000000..0dfc187de2 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/DegContext.java @@ -0,0 +1,54 @@ +/* ### + * 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 datagraph; + +import java.util.Set; + +import datagraph.data.graph.DegEdge; +import datagraph.data.graph.DegVertex; +import docking.ActionContext; +import ghidra.graph.viewer.actions.VgVertexContext; +import ghidra.graph.viewer.event.mouse.VertexMouseInfo; + +/** + * {@link ActionContext} for the data exploration graph. + */ +public class DegContext extends VgVertexContext { + + private Set selectedVertices; + + public DegContext(DataGraphProvider dataGraphProvider, DegVertex targetVertex, + Set selectedVertices) { + this(dataGraphProvider, targetVertex, selectedVertices, null); + } + + public DegContext(DataGraphProvider dataGraphProvider, + DegVertex targetVertex, Set selectedVertices, + VertexMouseInfo vertexMouseInfo) { + super(dataGraphProvider, targetVertex); + this.selectedVertices = selectedVertices; + } + + public Set getSelectedVertices() { + return selectedVertices; + } + + @Override + public boolean shouldShowSatelliteActions() { + return getVertex() == null; + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/DegSatelliteContext.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/DegSatelliteContext.java new file mode 100644 index 0000000000..cd4d39b7ea --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/DegSatelliteContext.java @@ -0,0 +1,32 @@ +/* ### + * 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 datagraph; + +import docking.ActionContext; +import ghidra.app.context.ProgramActionContext; +import ghidra.graph.viewer.actions.VisualGraphActionContext; + +/** + * {@link ActionContext} for the data exploration satellite graph. + */ +public class DegSatelliteContext extends ProgramActionContext + implements VisualGraphActionContext { + + public DegSatelliteContext(DataGraphProvider dataGraphProvider) { + super(dataGraphProvider, dataGraphProvider.getProgram()); + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/CodeDegVertex.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/CodeDegVertex.java new file mode 100644 index 0000000000..8cfd625bcf --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/CodeDegVertex.java @@ -0,0 +1,181 @@ +/* ### + * 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 datagraph.data.graph; + +import java.awt.Component; +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import javax.swing.JComponent; + +import datagraph.graph.explore.EgVertex; +import docking.action.DockingAction; +import ghidra.base.graph.CircleWithLabelVertexShapeProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; + +/** + * A vertex that represents code in a data exploration graph. Currently, code vertices are + * "dead end" vertices in the graph and cannot be explored further. + */ +public class CodeDegVertex extends DegVertex { + + private Instruction instruction; + private CircleWithLabelVertexShapeProvider shapeProvider; + + /** + * Constructor + * @param controller the graph controller + * @param instruction the instruction that is reference from/to a data object in the graph. + * @param parent the source vertex (from what vertex did you explore to get here) + */ + public CodeDegVertex(DegController controller, Instruction instruction, DegVertex parent) { + super(controller, parent); + this.instruction = instruction; + String label = getVertexLabel(); + + this.shapeProvider = new CircleWithLabelVertexShapeProvider(label); + shapeProvider.setTogglesVisible(false); + + } + + @Override + public String getTitle() { + return null; + } + + @Override + public int hashCode() { + return instruction.hashCode(); + } + + @Override + public DegVertexStatus refreshGraph(boolean checkDataTypes) { + return DegVertexStatus.VALID; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CodeDegVertex other = (CodeDegVertex) obj; + return instruction.equals(other.instruction); + } + + private String getVertexLabel() { + Address address = instruction.getAddress(); + Program program = instruction.getProgram(); + FunctionManager functionManager = program.getFunctionManager(); + Function f = functionManager.getFunctionContaining(address); + if (f != null) { + String name = f.getName(); + if (!f.getEntryPoint().equals(address)) { + name += " + " + address.subtract(f.getEntryPoint()); + } + return name; + } + return address.toString(); + } + + @Override + public void clearUserChangedLocation() { + super.clearUserChangedLocation(); + controller.relayoutGraph(); + } + + @Override + public String toString() { + return "Instruction @ " + instruction.getAddress().toString(); + } + + @Override + public Shape getCompactShape() { + return shapeProvider.getCompactShape(); + } + + @Override + public Shape getFullShape() { + return shapeProvider.getFullShape(); + } + + @Override + public JComponent getComponent() { + return shapeProvider.getComponent(); + } + + @Override + protected void addAction(DockingAction action) { + //codeVertexPanel.addAction(action); + } + + @Override + public Address getAddress() { + return instruction.getMinAddress(); + } + + @Override + public void setSelected(boolean selected) { + super.setSelected(selected); + controller.navigateOut(instruction.getAddress(), null); + } + + @Override + public CodeUnit getCodeUnit() { + return instruction; + } + + @Override + public boolean isGrabbable(Component component) { + return true; + } + + @Override + protected Point2D getStartingEdgePoint(EgVertex end) { + Point2D location = getLocation(); + return new Point2D.Double(location.getX(), + location.getY() + shapeProvider.getCircleCenterYOffset()); + } + + @Override + protected Point2D getEndingEdgePoint(EgVertex start) { + Point2D location = getLocation(); + return new Point2D.Double(location.getX(), + location.getY() + shapeProvider.getCircleCenterYOffset()); + } + + @Override + protected boolean containsAddress(Address address) { + return instruction.contains(address); + } + + @Override + public String getTooltip(MouseEvent e) { + return null; + } + + @Override + public int compare(DegVertex o1, DegVertex o2) { + return o1.getAddress().compareTo(o2.getAddress()); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataDegVertex.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataDegVertex.java new file mode 100644 index 0000000000..8ffc8b8a32 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataDegVertex.java @@ -0,0 +1,390 @@ +/* ### + * 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 datagraph.data.graph; + +import static datagraph.data.graph.DegVertex.DegVertexStatus.*; + +import java.awt.Dimension; +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.List; +import java.util.Set; + +import javax.swing.JComponent; + +import datagraph.DataGraphPlugin; +import datagraph.data.graph.panel.DataVertexPanel; +import datagraph.data.graph.panel.model.row.DataRowObject; +import datagraph.graph.explore.EgVertex; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.action.builder.ActionBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.Data; +import ghidra.util.HelpLocation; +import resources.Icons; + +/** + * A vertex in the data exploration graph for displaying the contents of a single data object. + */ +public class DataDegVertex extends DegVertex { + + private Data data; + private DataVertexPanel dataVertexPanel; + private DockingAction deleteAction; + private long dataTypeHash; + + /** + * Constructor + * @param controller the controller + * @param data the Data object to be displayed by this vertex + * @param source the source vertex (from what vertex did you explore to get here) + * @param compactFormat determines if the row displays are in a compact format or an expanded + * format + */ + public DataDegVertex(DegController controller, Data data, DegVertex source, + boolean compactFormat) { + super(controller, source); + this.data = data; + dataVertexPanel = new DataVertexPanel(controller, this, compactFormat); + createActions(); + dataVertexPanel.updateHeader(); + dataVertexPanel.updateShape(); + + if (source == null) { + dataVertexPanel.setIsRoot(true); + } + dataTypeHash = hash(data.getDataType()); + } + + @Override + public String getTitle() { + return dataVertexPanel.getTitle(); + } + + @Override + public DegVertexStatus refreshGraph(boolean checkDataType) { + Address address = data.getAddress(); + Data newData = data.getProgram().getListing().getDataAt(address); + if (newData == null) { + return MISSING; + } + if (data != newData) { + this.data = newData; + dataVertexPanel.setData(newData); + return CHANGED; + } + + if (checkDataType) { + long newHash = hash(data.getDataType()); + if (newHash != dataTypeHash) { + dataTypeHash = newHash; + dataVertexPanel.setData(data); // force the data model to reset + return CHANGED; + } + } + return VALID; + } + + /** + * {@return a list of vertex row objects (currently only used for testing).} + */ + public List getRowObjects() { + return dataVertexPanel.getRowObjects(); + } + + @Override + public void clearUserChangedLocation() { + super.clearUserChangedLocation(); + controller.relayoutGraph(); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + @Override + public void setSource(EgVertex source) { + super.setSource(source); + dataVertexPanel.setIsRoot(source == null); + deleteAction.setEnabled(source != null); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DataDegVertex other = (DataDegVertex) obj; + return data.equals(other.data); + } + + @Override + public JComponent getComponent() { + return dataVertexPanel; + } + + @Override + public String toString() { + return "Data @ " + data.getAddress().toString(); + } + + @Override + public Address getAddress() { + return data.getAddress(); + } + + @Override + public CodeUnit getCodeUnit() { + return data; + } + + @Override + protected void addAction(DockingAction action) { + dataVertexPanel.addAction(action); + } + + @Override + public DockingActionIf getAction(String name) { + return dataVertexPanel.getAction(name); + } + + @Override + public int getOutgoingEdgeOffsetFromCenter(EgVertex v) { + return dataVertexPanel.getOutgoingEdgeOffsetFromCenter(v); + } + + @Override + public int getIncomingEdgeOffsetFromCenter(EgVertex vertex) { + return dataVertexPanel.getIncommingEdgeOffsetFromCenter(vertex); + } + + @Override + public void setSelected(boolean selected) { + super.setSelected(selected); + dataVertexPanel.setSelected(selected); + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + dataVertexPanel.setFocused(focused); + } + + @Override + public void dispose() { + dataVertexPanel.dispose(); + dataVertexPanel = null; + } + + @Override + public Shape getCompactShape() { + Shape shape = dataVertexPanel.getShape(); + return shape; + } + + public Data getData() { + return data; + } + + public Dimension getSize() { + return dataVertexPanel.getSize(); + } + + /** + * Sets the size of this vertex by the user. + * @param dimension the new size for this vertex; + */ + public void setSizeByUser(Dimension dimension) { + dataVertexPanel.setSizeByUser(dimension); + } + + /** + * Records the componentPath of the sub-data in this vertex that the edge going to + * the end vertex is associated with. Used to compute the y coordinate of the edge so that + * aligns with that data as the data is scrolled within the component. + * @param end the vertex that our outgoing edge to attached + * @param componentPath the component path of the sub data in this vertex associated with + * the edge going to the end vertex + */ + public void addOutgoingEdgeAnchor(DegVertex end, int[] componentPath) { + dataVertexPanel.addOutgoingEdge(end, componentPath); + } + + /** + * Records the Address of the sub-data in this vertex that the edge coming in + * from the start vertex is associated with. Used to compute the y coordinate of the edge so + * that it aligns with the sub-data as the data is scrolled within the component. For incoming + * edges, we only record edges that are offset from the data start. If the incoming edge points + * to the overall data object, the edge will always be attached to the top of the vertex + * regardless of the scroll position. + * + * @param start the vertex that associated with an incoming edge + * @param address the address of the sub data in this vertex associated with + * the edge coming from the start vertex. + */ + public void addIncomingEdgeAnchor(DegVertex start, Address address) { + dataVertexPanel.addIncommingEdge(start, address); + } + + @Override + public int compare(DegVertex v1, DegVertex v2) { + // outgoing child vertices are ordered based on the paths of the data that references + // them so that they are in the same order they appear in the referring structure datatype. + return dataVertexPanel.comparePaths(v1, v2); + } + + private void createActions() { + String owner = DataGraphPlugin.class.getSimpleName(); + if (dataVertexPanel.isExpandable()) { + DockingAction openAllAction = new ActionBuilder("Expand All", owner) + .toolBarIcon(Icons.EXPAND_ALL_ICON) + .description("Recursively open all data in this vertex.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Expand_All")) + .onAction(c -> dataVertexPanel.expandAll()) + .build(); + addAction(openAllAction); + DockingAction closeAllAction = new ActionBuilder("Collapse All", owner) + .toolBarIcon(Icons.COLLAPSE_ALL_ICON) + .description("Close all data in this vertex.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Collapse_All")) + .onAction(c -> dataVertexPanel.collapseAll()) + .build(); + addAction(closeAllAction); + } + + deleteAction = new ActionBuilder("Close Vertex", owner) + .toolBarIcon(Icons.CLOSE_ICON) + .description("Removes this vertex and any of its descendents from the graph.") + .helpLocation(new HelpLocation("DataGraphPlugin", "Delete_Vertex")) + .enabled(source != null) + .onAction(c -> controller.deleteVertices(Set.of(DataDegVertex.this))) + .build(); + addAction(deleteAction); + + } + + @Override + public String getTooltip(MouseEvent e) { + return dataVertexPanel.getToolTipText(e); + } + + @Override + protected Point2D getStartingEdgePoint(EgVertex end) { + Point2D startLocation = getLocation(); + + // For the edge leaving this vertex going to the given end vertex, we need the + // starting point of the edge. + // + // We do this by starting with this vertex's location which is at the center point in + // the vertex. We want the x coordinate to be on the right edge of the vertex, so we add in + // half the width. We want the y coordinate to be wherever the corresponding data element + // is being displayed, so we need to know how much above or below the center point + // to draw the edge point to make it line up in the scrolled display. + + int yOffset = getOutgoingEdgeOffsetFromCenter(end); + double x = startLocation.getX() + getSize().width / 2; + double y = startLocation.getY() + yOffset; + return new Point2D.Double(x, y); + } + + @Override + protected Point2D getEndingEdgePoint(EgVertex start) { + Point2D endLocation = getLocation(); + + // For the edge entering this vertex from the given start vertex, we need the + // ending point of the edge. + // + // We do this by starting with this vertex's location which is at the center point in + // the vertex. We want the x coordinate to be on the left edge of the vertex, so we subtract + // half the width. We want the y coordinate to be wherever the corresponding address of + // the reference is being displayed, so we need to know how much above or below the center + // point to draw the edge point to make it line up in the scrolled display. + + int yOffset = getIncomingEdgeOffsetFromCenter(start); + + double x = endLocation.getX() - getSize().width / 2; + double y = endLocation.getY() + yOffset; + return new Point2D.Double(x, y); + } + + /** + * Sets whether the column model should be a compact format or an expanded format. The basic + * difference is that expanded format includes a datatype for each row and the compact only + * shows a datatype if there is no value. + * @param b true to show a compact row, false to show more information. + */ + public void setCompactFormat(boolean b) { + dataVertexPanel.setCompactFormat(b); + } + + /** + * Opens the given data row to show its sub-data components. + * @param row the row to expand + */ + public void expand(int row) { + dataVertexPanel.expand(row); + } + + /** + * Adds a new vertex following the reference(s) coming out of the data component on the given + * row. + * @param row the row to open new vertices from + */ + public void openPointerReference(int row) { + dataVertexPanel.openPointerReference(row); + } + + /** + * {@return true if the selected row in the vertex is expandable.} + */ + public boolean isOnExpandableRow() { + return dataVertexPanel.isSelectedRowExpandable(); + } + + /** + * Expands the selected row in the vertex recursively. + */ + public void expandSelectedRowRecursively() { + dataVertexPanel.expandSelectedRowRecursively(); + } + + @Override + protected boolean containsAddress(Address address) { + return data.contains(address); + } + + private long hash(DataType dataType) { + long hash = dataType.getLength() * 31 + dataType.getName().hashCode(); + if (dataType instanceof Composite composite) { + for (DataTypeComponent dataTypeComponent : composite.getDefinedComponents()) { + hash = 31 * hash + hash(dataTypeComponent.getDataType()); + } + } + return hash; + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataExplorationGraph.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataExplorationGraph.java new file mode 100644 index 0000000000..2b1e46e994 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DataExplorationGraph.java @@ -0,0 +1,61 @@ +/* ### + * 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 datagraph.data.graph; + +import datagraph.graph.explore.AbstractExplorationGraph; +import ghidra.graph.viewer.layout.VisualGraphLayout; + +/** + * A graph for exploring data and its incoming and outgoing references. + */ +public class DataExplorationGraph extends AbstractExplorationGraph { + + private VisualGraphLayout layout; + + /** + * The initial vertex for the graph. All other vertices in this graph can trace back its source + * to this vertex. + * @param root the initial source vertex for this explore graph + */ + public DataExplorationGraph(DegVertex root) { + super(root); + } + + @Override + public VisualGraphLayout getLayout() { + return layout; + } + + @Override + public DataExplorationGraph copy() { + DataExplorationGraph newGraph = new DataExplorationGraph(getRoot()); + + for (DegVertex v : vertices.keySet()) { + newGraph.addVertex(v); + } + + for (DegEdge e : edges.keySet()) { + newGraph.addEdge(e); + } + + return newGraph; + } + + void setLayout(VisualGraphLayout layout) { + this.layout = layout; + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegController.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegController.java new file mode 100644 index 0000000000..93050548b0 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegController.java @@ -0,0 +1,749 @@ +/* ### + * 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 datagraph.data.graph; + +import static ghidra.framework.model.DomainObjectEvent.*; +import static ghidra.program.util.ProgramEvent.*; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Point2D; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.JComponent; + +import datagraph.DataGraphProvider; +import datagraph.data.graph.DegVertex.DegVertexStatus; +import edu.uci.ics.jung.visualization.control.AbstractGraphMousePlugin; +import ghidra.app.util.XReferenceUtils; +import ghidra.framework.model.DomainObjectChangedEvent; +import ghidra.framework.model.DomainObjectListener; +import ghidra.graph.job.RelayoutAndCenterVertexGraphJob; +import ghidra.graph.job.RelayoutAndEnsureVisible; +import ghidra.graph.viewer.*; +import ghidra.graph.viewer.event.mouse.VertexMouseInfo; +import ghidra.graph.viewer.event.mouse.VisualGraphPluggableGraphMouse; +import ghidra.graph.viewer.layout.VisualGraphLayout; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceManager; +import ghidra.program.util.ProgramLocation; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * The controller for managing and controlling a DataExplorationGraph + */ +public class DegController implements DomainObjectListener { + // max number of references to add automatically. 50 is just a guess as to what is reasonable + private static final int MAX_REFS = 50; + private DegGraphView view; + private DataExplorationGraph graph; + private DegLayoutProvider layoutProvider = new DegLayoutProvider(); + private Program program; + private boolean navigateOut = true; + private boolean navigateIn = false; + private boolean compactFormat = true; + private DataGraphProvider provider; + + /** + * Constructs a new data exploration graph controller. + * @param provider The data graph provider that created this controller + * @param data the initial data to display in the graph + */ + public DegController(DataGraphProvider provider, Data data) { + this.provider = provider; + this.program = data.getProgram(); + view = new DegGraphView(); + DegVertex root = new DataDegVertex(this, data, null, true); + graph = new DataExplorationGraph(root); + graph.setLayout(getLayout()); + view.setGraph(graph); + installMouseListeners(); + program.addListener(this); + setFocusedVertex(root); + } + + /** + * If outgoing navigation is on, navigate the tool to the selected graph location. + * @param address the address associated with the current graph location + * @param componentPath the component path associated with the current graph location + */ + public void navigateOut(Address address, int[] componentPath) { + if (navigateOut) { + ProgramLocation location = + new ProgramLocation(program, address, address, componentPath, null, 0, 0, 0); + provider.navigateOut(location); + } + } + + public void dispose() { + program.removeListener(this); + } + + public void repaint() { + view.repaint(); + } + + public DegLayoutProvider getLayoutProvider() { + return (DegLayoutProvider) view.getLayoutProvider(); + } + + public Component getComponent() { + return view.getViewComponent(); + } + + public void relayoutGraph() { + VisualGraphViewUpdater viewUpdater = view.getViewUpdater(); + viewUpdater.relayoutGraph(); + } + + /** + * Center the graph on the given vertex. + * @param vertex the vertex to center the graph on + */ + public void centerVertex(DegVertex vertex) { + VisualGraphViewUpdater viewUpdater = view.getViewUpdater(); + viewUpdater.moveVertexToCenterWithAnimation(vertex); + } + + public void centerPoint(Point point) { + VisualGraphViewUpdater viewUpdater = view.getViewUpdater(); + viewUpdater.moveViewerLocationWithoutAnimation(point); + } + + /** + * Removes the given vertices from the graph. + * @param selectedVertices the set of vertices to remove + */ + public void deleteVertices(Set selectedVertices) { + Set toRemove = new HashSet<>(); + + for (DegVertex dgVertex : selectedVertices) { + if (dgVertex.isRoot()) { + continue; // can't delete root vertex + } + if (!toRemove.contains(dgVertex)) { + toRemove.addAll(graph.getDescendants(dgVertex)); + } + toRemove.add(dgVertex); + } + + graph.removeVertices(toRemove); + repaint(); + } + + /** + * Add new vertices and edges to all references from the given sub data. + * @param source the vertex that is being explored out of + * @param subData the data within the source vertex that has outgoing references that we + * are adding to the graph + */ + public void addOutGoingReferences(DataDegVertex source, Data subData) { + List addedVertices = new ArrayList<>(); + doAddOutGoingReferences(source, subData, addedVertices); + if (!addedVertices.isEmpty()) { + relayoutGraphAndEnsureVisible(source, addedVertices.getLast()); + } + } + + /** + * Add new vertices for all incoming references to the given vertex's data (including offcut) + * @param sourceVertex the vertex for which to add new incoming reference vertices + */ + public void showAllIncommingReferences(DataDegVertex sourceVertex) { + CodeUnit target = sourceVertex.getCodeUnit(); + Listing listing = program.getListing(); + List refs = XReferenceUtils.getXReferences(target, MAX_REFS + 1); + if (refs.size() < MAX_REFS) { + int offcutMax = MAX_REFS + 1 - refs.size(); + List offcuts = XReferenceUtils.getOffcutXReferences(target, offcutMax); + refs.addAll(offcuts); + } + if (refs.size() > MAX_REFS) { + Msg.showWarn(this, null, "Too Many References", + "Only showing the first " + MAX_REFS + " number of references"); + refs = refs.subList(0, MAX_REFS); + } + if (refs.isEmpty()) { + Msg.showInfo(this, null, "No References Found", + "There were no references or offcut references found to this data."); + return; + } + + doAddIncomingReferences(sourceVertex, listing, refs); + + } + + /** + * Add new vertices for all outgoing references from the given vertex's data (including + * sub-data) + * @param sourceVertex the vertex for which to add new outgoing reference vertices + */ + public void showAllOutgoingReferences(DataDegVertex sourceVertex) { + Data data = sourceVertex.getData(); + int edgeCount = graph.getEdgeCount(); + List addedVertices = new ArrayList<>(); + if (!showAllOutgoingReferencesRecursively(sourceVertex, data, addedVertices)) { + Msg.showWarn(this, null, "Too Many References", + "Only added the first " + MAX_REFS + " references"); + } + + if (!addedVertices.isEmpty()) { + relayoutGraphAndEnsureVisible(sourceVertex, addedVertices.getLast()); + } + else if (graph.getEdgeCount() != edgeCount) { + relayoutGraph(); + } + else { + Msg.showInfo(data, null, "No New Outgoing References Found", + "There were no additional references found to this data."); + } + } + + /** + * Sets the graph's root (home) vertex. This will completely rearrange the graph as though + * the given vertex was the original vertex that all the other vertexes were explored from. + * @param newRoot the new root source vertex (original explore vertex) + */ + public void orientAround(DegVertex newRoot) { + graph.setRoot(newRoot); + relayoutGraphAndCenter(newRoot); + } + + public VisualGraphView getView() { + return view; + } + + public DegVertex getFocusedVertex() { + return view.getFocusedVertex(); + } + + public Program getProgram() { + return program; + } + + /** + * Turns on or off outgoing program location navigation + * @param b if true, clicking in the graph will cause the tools location to change. If false + * clicking the graph will not affect the rest of the tool. + */ + public void setNavigateOut(boolean b) { + navigateOut = b; + } + + /** + * Turns on if this graph should track tool location events. + * @param b if true, the graph will try and select the vertex associated with the current tool's + * location. If false, the graph will be unaffected by tool program location changes. + */ + public void setNavigateIn(boolean b) { + navigateIn = b; + } + + /** + * Sets if the tool should display data in a compact format or a more detailed format. + * @param b true for compact, false for detailed + */ + public void setCompactFormat(boolean b) { + compactFormat = b; + graph.getVertices().forEach(v -> { + if (v instanceof DataDegVertex dataVertex) { + dataVertex.setCompactFormat(b); + } + }); + relayoutGraph(); + } + + public boolean isCompactFormat() { + return compactFormat; + } + + /** + * Clears any user set location and positions all the vertices to the standard computed layout. + */ + public void resetAndRelayoutGraph() { + graph.getVertices().forEach(v -> v.clearUserChangedLocation()); + relayoutGraph(); + } + + @Override + public void domainObjectChanged(DomainObjectChangedEvent ev) { + if (ev.contains(RESTORED)) { + refreshGraph(true); + return; + } + boolean dataTypesChanged = ev.contains(DATA_TYPE_CHANGED, DATA_TYPE_REMOVED); + boolean refreshNeeded = ev.contains(CODE_REMOVED) | dataTypesChanged; + if (refreshNeeded) { + refreshGraph(dataTypesChanged); + } + repaint(); + } + + /** + * {@return a collection of vertices that can be reached by following outgoing edges.} + * @param vertex the vertex to get outgoing vertices for + */ + public Set getOutgoingVertices(DataDegVertex vertex) { + Collection outEdges = graph.getOutEdges(vertex); + return outEdges.stream().map(e -> e.getEnd()).collect(Collectors.toSet()); + } + + /** + * {@return a collection of vertices that can be reached by following incoming edges.} + * @param vertex the vertex to get incoming vertices for + */ + public Set getIncomingVertices(DataDegVertex vertex) { + Collection inEdges = graph.getOutEdges(vertex); + return inEdges.stream().map(e -> e.getStart()).collect(Collectors.toSet()); + } + + public DataExplorationGraph getGraph() { + return graph; + } + + public GraphViewer getPrimaryViewer() { + return view.getPrimaryGraphViewer(); + } + + /** + * Informs the controller that the tool's location changed. If navigation in is turned on, + * this will attempt to find the vertex matching the given location. If a match is found, the + * graph will select that vertex. + * @param location the new location for the tool + */ + public void locationChanged(ProgramLocation location) { + if (!navigateIn) { + return; + } + if (program != location.getProgram()) { + return; + } + Address address = location.getAddress(); + Collection vertices = graph.getVertices(); + for (DegVertex dgVertex : vertices) { + if (dgVertex.containsAddress(address)) { + navigateTo(dgVertex); + break; + } + } + + } + + private void doAddIncomingReferences(DataDegVertex sourceVertex, Listing listing, + List xReferences) { + boolean vertexOrEdgeAdded = false; + for (Reference reference : xReferences) { + vertexOrEdgeAdded |= doAddIncomingVertices(sourceVertex, listing, reference); + + } + if (!vertexOrEdgeAdded) { + Msg.showInfo(this, null, "No New References Found", + "There were no additional references or offcut references found to this data."); + } + } + + private boolean doAddIncomingVertices(DataDegVertex sourceVertex, Listing listing, + Reference reference) { + Address toAddress = reference.getToAddress(); + Address fromAddress = reference.getFromAddress(); + CodeUnit cu = listing.getCodeUnitContaining(fromAddress); + DegVertex v = createVertex(cu, sourceVertex); + + // if vertex is new, add it and the edge + if (!graph.containsVertex(v)) { + graph.getLayout().setLocation(v, sourceVertex.getLocation()); + graph.addVertex(v); + DegEdge newEdge = new DegEdge(v, sourceVertex); + addIncomingEdge(fromAddress, toAddress, cu, newEdge); + return true; + } + + // see if we need to just add an edge + v = resolve(v); + DegEdge newEdge = new DegEdge(v, sourceVertex); + if (!graph.containsEdge(newEdge)) { + addIncomingEdge(fromAddress, toAddress, cu, newEdge); + return true; + } + return false; + } + + private void addIncomingEdge(Address fromAddress, Address toAddress, + CodeUnit cu, DegEdge newEdge) { + graph.addEdge(newEdge); + DegVertex newVertex = newEdge.getStart(); + DataDegVertex sourceVertex = (DataDegVertex) newEdge.getEnd(); + if (newVertex instanceof DataDegVertex dv) { + dv.addOutgoingEdgeAnchor(sourceVertex, findComponentPath((Data) cu, fromAddress)); + } + sourceVertex.addIncomingEdgeAnchor(newVertex, toAddress); + relayoutGraphAndEnsureVisible(sourceVertex, newVertex); + } + + private DegVertex addDestinationVertex(DataDegVertex sourceVertex, Address a, + Data sourceData) { + + CodeUnit cu = program.getListing().getCodeUnitContaining(a); + if (cu == null) { + return null; + } + + DegVertex v = createVertex(cu, sourceVertex); + if (!graph.containsVertex(v)) { + graph.getLayout().setLocation(v, sourceVertex.getLocation()); + graph.addVertex(v); + graph.addEdge(new DegEdge(sourceVertex, v)); + sourceVertex.addOutgoingEdgeAnchor(v, sourceData.getComponentPath()); + if (v instanceof DataDegVertex dataVertex) { + dataVertex.addIncomingEdgeAnchor(sourceVertex, a); + } + return v; // this is the only case where we actually added a new vertex + } + + // we already had the vertex in the graph, but we may need to add an edge + if (!graph.containsEdge(new DegEdge(sourceVertex, v))) { + v = resolve(v); + graph.addEdge(new DegEdge(sourceVertex, v)); + sourceVertex.addOutgoingEdgeAnchor(v, sourceData.getComponentPath()); + if (v instanceof DataDegVertex dataVertex) { + dataVertex.addIncomingEdgeAnchor(sourceVertex, a); + } + } + else { + // if a vertex has multiple references to the same location, this will move the + // outgoing edge offset to this component (taking from any other outgoing component + // to the same location). + sourceVertex.addOutgoingEdgeAnchor(v, sourceData.getComponentPath()); + } + return null; // the vertex already exited in the graph + } + + private Set
getOutgoingReferenceAddresses(Data data) { + Set
destinations = new HashSet<>(); + ReferenceManager referenceManager = program.getReferenceManager(); + Address fromAddress = data.getAddress(); + Reference[] referencesFrom = referenceManager.getReferencesFrom(fromAddress); + for (Reference reference : referencesFrom) { + destinations.add(reference.getToAddress()); + } + Object value = data.getValue(); + if (value instanceof Address address) { + destinations.add(address); + } + return destinations; + } + + private DegVertex createVertex(CodeUnit cu, DegVertex source) { + if (cu instanceof Data data) { + return new DataDegVertex(this, data, source, compactFormat); + } + return new CodeDegVertex(this, (Instruction) cu, source); + } + + void relayoutGraphAndEnsureVisible(DegVertex primary, DegVertex secondary) { + if (primary == null || secondary == null) { + relayoutGraph(); + return; + } + VisualGraphViewUpdater viewUpdater = view.getViewUpdater(); + GraphViewer viewer = view.getPrimaryGraphViewer(); + RelayoutAndEnsureVisible job = + new RelayoutAndEnsureVisible<>(viewer, primary, secondary, + viewUpdater.isAnimationEnabled()); + + viewUpdater.scheduleViewChangeJob(job); + } + + void relayoutGraphAndCenter(DegVertex vertex) { + + VisualGraphViewUpdater viewUpdater = view.getViewUpdater(); + GraphViewer viewer = view.getPrimaryGraphViewer(); + RelayoutAndCenterVertexGraphJob job = + new RelayoutAndCenterVertexGraphJob<>(viewer, vertex, viewUpdater.isAnimationEnabled()); + + viewUpdater.scheduleViewChangeJob(job); + } + + private DegVertex resolve(DegVertex searchVertex) { + for (DegVertex v : graph.getVertices()) { + if (v.equals(searchVertex)) { + return v; + } + } + return null; + } + + private boolean showAllOutgoingReferencesRecursively(DataDegVertex sourceVertex, Data data, + List added) { + if (data.getNumComponents() == 0) { + doAddOutGoingReferences(sourceVertex, data, added); + } + + for (int i = 0; i < data.getNumComponents(); i++) { + if (added.size() >= MAX_REFS) { + return false; + } + Data subData = data.getComponent(i); + if (!showAllOutgoingReferencesRecursively(sourceVertex, subData, added)) { + return false; + } + } + return true; + } + + private void doAddOutGoingReferences(DataDegVertex source, Data data, List added) { + Set
addresses = getOutgoingReferenceAddresses(data); + + for (Address address : addresses) { + DegVertex newVertex = addDestinationVertex(source, address, data); + if (newVertex != null) { + added.add(newVertex); + } + } + } + + private int[] findComponentPath(Data data, Address fromAddress) { + if (data == null) { + return new int[0]; + } + int offset = (int) fromAddress.subtract(data.getAddress()); + if (offset == 0) { + return data.getComponentPath(); + } + Data componentContaining = data.getComponentContaining(offset); + return findComponentPath(componentContaining, fromAddress); + + } + + private void refreshGraph(boolean checkDataTypes) { + + Set toDelete = new HashSet<>(); + for (DegVertex dgVertex : graph.getVertices()) { + DegVertexStatus status = dgVertex.refreshGraph(checkDataTypes); + + if (status == DegVertexStatus.MISSING) { + toDelete.add(dgVertex); + toDelete.addAll(graph.getDescendants(dgVertex)); + } + else if (status == DegVertexStatus.CHANGED) { + // Since the datatype for this vertex changed, we must eliminate all outgoing edges + // since they may be invalid now. To remove an edge we need to either remove the + // changed node or the node it points to. + removeOutgoingEdges(toDelete, dgVertex); + } + } + graph.removeVertices(toDelete); + } + + private void removeOutgoingEdges(Set toDelete, DegVertex dgVertex) { + Collection outEdges = graph.getOutEdges(dgVertex); + for (DegEdge edge : outEdges) { + DegVertex other = edge.getEnd(); + + // If the other vertex is a descendent of this vertex, it and all its + // descendants should be removed. Otherwise, this vertex is a descendent of the + // other vertex and therefore it should be removed. This is + // because when pruning nodes, you must always prune descendant sub trees. + + if (other.getSourceVertex() == dgVertex) { + toDelete.add(other); + toDelete.addAll(graph.getDescendants(other)); + } + else { + // else dgVertex is the descendent of the other vertx, so dgVertex and its + // descendants should be removed + toDelete.add(dgVertex); + toDelete.addAll(graph.getDescendants(dgVertex)); + } + } + } + + /** + * Selects and centers the original source vertex + */ + public void selectAndCenterHomeVertex() { + navigateTo(graph.getRoot()); + } + + private void navigateTo(DegVertex dgVertex) { + setFocusedVertex(dgVertex); + centerVertex(dgVertex); + } + + private void installMouseListeners() { + VisualGraphPluggableGraphMouse graphMouse = + view.getPrimaryGraphViewer().getGraphMouse(); + + graphMouse.prepend(new DataMousePlugin()); + } + + private VisualGraphLayout getLayout() { + try { + return layoutProvider.getLayout(graph, TaskMonitor.DUMMY); + } + catch (CancelledException e) { + return null; + } + } + + private void setFocusedVertex(DegVertex v) { + view.getGraphComponent().setVertexFocused(v); + } + + private boolean isOnResizeCorner(VertexMouseInfo vertexMouseInfo) { + DegVertex vertex = vertexMouseInfo.getVertex(); + Point2D p = vertexMouseInfo.getVertexRelativeClickPoint(); + Dimension vertexSize = vertex.getComponent().getSize(); + return p.getX() > vertexSize.width - 10 && p.getY() > vertexSize.height - 10; + } + + /** + * Mouse plugin for the data explore graph. Mainly used to forward mouse wheel events so thet + * the scrollable vertex components can be scrolled via the mouse wheel. Also, is used used + * to manually resize a node. + */ + private class DataMousePlugin extends AbstractGraphMousePlugin + implements MouseWheelListener, MouseMotionListener, MouseListener { + + private VertexMouseInfo dragStart; + private Dimension startSize; + private boolean isFiringWheelEvent; + + public DataMousePlugin() { + super(0); + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (isFiringWheelEvent) { + return; + } + VertexMouseInfo vertexMouseInfo = getTranslatedMouseInfo(e); + if (vertexMouseInfo == null) { + return; + } + + if (vertexMouseInfo.isScaledPastInteractionThreshold()) { + return; + } + if (e.getModifiersEx() != 0) { + // let graph handle modified mouse wheel events + return; + } + + try { + isFiringWheelEvent = true; + vertexMouseInfo.forwardEvent(); + } + finally { + isFiringWheelEvent = false; + } + repaint(); + } + + private VertexMouseInfo getTranslatedMouseInfo(MouseEvent e) { + GraphViewer viewer = view.getPrimaryGraphViewer(); + return GraphViewerUtils.convertMouseEventToVertexMouseEvent(viewer, e); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragStart == null) { + return; + } + MouseEvent startEv = dragStart.getOriginalMouseEvent(); + int x = e.getX() - startEv.getX(); + int y = e.getY() - startEv.getY(); + + DataDegVertex vertex = (DataDegVertex) dragStart.getVertex(); + int newWidth = Math.max(startSize.width + x, 50); + int newHeight = Math.max(startSize.height + y, 50); + vertex.setSizeByUser(new Dimension(newWidth, newHeight)); + } + + @Override + public void mouseMoved(MouseEvent e) { + VertexMouseInfo vertexMouseInfo = getTranslatedMouseInfo(e); + if (vertexMouseInfo == null) { + return; + } + + if (vertexMouseInfo.isScaledPastInteractionThreshold()) { + return; + } + JComponent c = (JComponent) vertexMouseInfo.getEventSource(); + if (isOnResizeCorner(vertexMouseInfo)) { + c.setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR)); + // we need to consume the event or else a follow-on mouse + // process may change the cursor + e.consume(); + } + else { + // here we don't want to consume the event because we WANT follow on mouse + // processing to possibly change the cursor + c.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + // not currently used + } + + @Override + public void mousePressed(MouseEvent e) { + VertexMouseInfo vertexMouseInfo = getTranslatedMouseInfo(e); + if (vertexMouseInfo == null) { + return; + } + + if (vertexMouseInfo.isScaledPastInteractionThreshold()) { + return; + } + DegVertex vertex = vertexMouseInfo.getVertex(); + if (vertex instanceof DataDegVertex dataVertex && isOnResizeCorner(vertexMouseInfo)) { + dragStart = vertexMouseInfo; + startSize = dataVertex.getSize(); + } + + } + + @Override + public void mouseReleased(MouseEvent e) { + if (dragStart != null) { + relayoutGraph(); + dragStart = null; + } + } + + @Override + public void mouseEntered(MouseEvent e) { + // not currently used + } + + @Override + public void mouseExited(MouseEvent e) { + // not currently used + } + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegEdge.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegEdge.java new file mode 100644 index 0000000000..37f445c398 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegEdge.java @@ -0,0 +1,35 @@ +/* ### + * 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 datagraph.data.graph; + +import datagraph.graph.explore.EgEdge; + +/** + * An edge for the {@link DataExplorationGraph} + */ +public class DegEdge extends EgEdge { + + public DegEdge(DegVertex start, DegVertex end) { + super(start, end); + } + + @SuppressWarnings("unchecked") + // Suppressing warning on the return type; we know our class is the right type + @Override + public DegEdge cloneEdge(DegVertex start, DegVertex end) { + return new DegEdge(start, end); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegGraphView.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegGraphView.java new file mode 100644 index 0000000000..8133c99ef8 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegGraphView.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 datagraph.data.graph; + +import java.awt.event.MouseEvent; + +import javax.swing.JComponent; + +import ghidra.graph.viewer.GraphViewer; +import ghidra.graph.viewer.VisualGraphView; +import ghidra.graph.viewer.event.mouse.VertexTooltipProvider; + +/** + * Extends the VisualGraphView mainly to provide appropriate tool tips. + */ +public class DegGraphView extends VisualGraphView { + DegGraphView() { + super(); + setSatelliteVisible(false); + } + + @Override + protected void installGraphViewer() { + super.installGraphViewer(); + GraphViewer viewer = graphComponent.getPrimaryViewer(); + viewer.setVertexTooltipProvider(new DataGraphVertexTipProvider()); + } + + private class DataGraphVertexTipProvider implements VertexTooltipProvider { + + @Override + public JComponent getTooltip(DegVertex v) { + return null; + } + + @Override + public JComponent getTooltip(DegVertex v, DegEdge e) { + return null; + } + + @Override + public String getTooltipText(DegVertex v, MouseEvent e) { + return v.getTooltip(e); + } + + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayout.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayout.java new file mode 100644 index 0000000000..41811f13b0 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayout.java @@ -0,0 +1,71 @@ +/* ### + * 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 datagraph.data.graph; + +import java.util.Comparator; + +import datagraph.graph.explore.EgEdgeTransformer; +import datagraph.graph.explore.EgGraphLayout; +import ghidra.graph.VisualGraph; +import ghidra.graph.viewer.layout.AbstractVisualGraphLayout; + +/** + * The layout for the DataExplorationGraph. It extends the {@link EgGraphLayout} the implements + * the basic incoming and outgoing tree structures. This basically just adds the ordering logic + * for vertices. + */ +public class DegLayout extends EgGraphLayout { + + protected DegLayout(DataExplorationGraph g, int verticalGap, int horizontalGap) { + super(g, "Data Graph Layout", verticalGap, horizontalGap); + } + + @Override + public DataExplorationGraph getVisualGraph() { + return (DataExplorationGraph) getGraph(); + } + + @Override + public AbstractVisualGraphLayout createClonedLayout( + VisualGraph newGraph) { + if (!(newGraph instanceof DataExplorationGraph dataGraph)) { + throw new IllegalArgumentException( + "Must pass a " + DataExplorationGraph.class.getSimpleName() + + "to clone the " + getClass().getSimpleName()); + } + + DegLayout newLayout = new DegLayout(dataGraph, verticalGap, horizontalGap); + return newLayout; + } + + @Override + protected Comparator getIncommingVertexComparator() { + return (v1, v2) -> v1.getAddress().compareTo(v2.getAddress()); + } + + @Override + protected Comparator getOutgoingVertexComparator() { + return (v1, v2) -> { + DegVertex parent = (DegVertex) v1.getSourceVertex(); + return parent.compare(v1, v2); + }; + } + + @Override + protected EgEdgeTransformer createEdgeTransformer() { + return new EgEdgeTransformer<>(); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayoutProvider.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayoutProvider.java new file mode 100644 index 0000000000..0f1dadbaab --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegLayoutProvider.java @@ -0,0 +1,46 @@ +/* ### + * 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 datagraph.data.graph; + +import ghidra.graph.viewer.layout.AbstractLayoutProvider; +import ghidra.graph.viewer.layout.VisualGraphLayout; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Provider for the DegLayout + */ +public class DegLayoutProvider + extends AbstractLayoutProvider { + private static final String NAME = "Data Graph Layout"; + + private static final int VERTICAL_GAP = 50; + private static final int HORIZONTAL_GAP = 100; + + @Override + public VisualGraphLayout getLayout(DataExplorationGraph graph, + TaskMonitor monitor) + throws CancelledException { + DegLayout layout = new DegLayout(graph, VERTICAL_GAP, HORIZONTAL_GAP); + initVertexLocations(graph, layout); + return layout; + } + + @Override + public String getLayoutName() { + return NAME; + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegVertex.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegVertex.java new file mode 100644 index 0000000000..c295357854 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/DegVertex.java @@ -0,0 +1,116 @@ +/* ### + * 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 datagraph.data.graph; + +import java.awt.Component; +import java.awt.event.MouseEvent; +import java.util.Comparator; + +import datagraph.graph.explore.EgVertex; +import docking.GenericHeader; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import ghidra.graph.viewer.vertex.VertexShapeProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.CodeUnit; + +/** + * A vertex for the {@DataExorationGraph} + */ +public abstract class DegVertex extends EgVertex implements VertexShapeProvider, + Comparator { + // enum for returning that status of a vertex when refreshing after a program change + public enum DegVertexStatus { + VALID, + MISSING, + CHANGED + } + + protected DegController controller; + + /** + * Constructor + * @param controller the controller for the data exploration graph + * @param source the vertex that spawned this vertex. The original source vertex has no + * parent source, but all the others must have a source which can be used to trace back + * to the original source vertex. + */ + public DegVertex(DegController controller, DegVertex source) { + super(source); + this.controller = controller; + } + + @Override + public boolean isGrabbable(Component component) { + Component c = component; + while (c != null) { + if (c instanceof GenericHeader) { + return true; + } + c = c.getParent(); + } + return false; + } + + /** + *{@return the program's address associated with this node.} + */ + public abstract Address getAddress(); + + /** + *{@return the CodeUnit associated with this node.} + */ + public abstract CodeUnit getCodeUnit(); + + /** + * {@return the tooltip for this vertex} + * @param e the the mouse even triggering this call + */ + public abstract String getTooltip(MouseEvent e); + + /** + * Checks if the given vertex is still valid after a program change. + * @param checkDataTypes if true, the underlying datatype should also be checked + * @return the status of the vertex. The vertex can be valid, changed, or missing. + */ + public abstract DegVertexStatus refreshGraph(boolean checkDataTypes); + + /** + * {@return the title of this vertex} + */ + public abstract String getTitle(); + + /** + * {@return true if code unit associated with this vertex contains the given address.} + * @param address the address to check if it is at or in this vertex + */ + protected abstract boolean containsAddress(Address address); + + /** + * {@return the docking action with the given name from this vertex.} + * @param name the name of the action to retrieve + */ + public DockingActionIf getAction(String name) { + return null; + } + + /** + * Adds the given action to this vertex. + * @param action the action to add to this vertex + */ + protected abstract void addAction(DockingAction action); + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DataVertexPanel.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DataVertexPanel.java new file mode 100644 index 0000000000..fdecb8b583 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DataVertexPanel.java @@ -0,0 +1,656 @@ +/* ### + * 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 datagraph.data.graph.panel; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.util.List; + +import javax.swing.*; + +import datagraph.data.graph.*; +import datagraph.data.graph.panel.model.column.CompactDataColumnModel; +import datagraph.data.graph.panel.model.column.ExpandedDataColumnModel; +import datagraph.data.graph.panel.model.row.DataRowObject; +import datagraph.data.graph.panel.model.row.DataTrableRowModel; +import datagraph.graph.explore.EgVertex; +import docking.GenericHeader; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.widgets.trable.GTrable; +import docking.widgets.trable.GTrableColumnModel; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Data; +import ghidra.util.datastruct.Range; +import resources.Icons; + +/** + * Main component to be displayed in a data {@link DegVertex}. It consists of a generic header and + * a scrollable GTrable that displays the elements of a {@link Data} item. + */ +public class DataVertexPanel extends JPanel { + private GenericHeader genericHeader; + private GTrable gTrable; + private DataTrableRowModel model; + private JScrollPane scroll; + private int headerHeight; + private Comparator pathComparator = new DtComponentPathComparator(); + + private Dimension userSize; + + private Rectangle preferredShape = new Rectangle(0, 0, 0, 0); + private DataDegVertex vertex; + private DegController controller; + + private Map incomingEdgeOffsetMap = new HashMap<>(); + private Map outgoingEdgeOffsetMap = new HashMap<>(); + private boolean cachedOutgoingOffsetsValid; + private boolean cachedIncomingOffsetsValid; + private GTrableColumnModel dataColumnModel; + + /** + * Constructor + * @param controller the data exploration graph controller + * @param vertex the vertex that created this DataVertexPanel + * @param compactFormat true if a compact format should be used to display the data + */ + public DataVertexPanel(DegController controller, DataDegVertex vertex, boolean compactFormat) { + super(new BorderLayout()); + this.controller = controller; + this.vertex = vertex; + buildComponent(vertex.getData(), compactFormat); + addKeyListener(new DataVertexKeyListener()); + updateTitle(); + headerHeight = genericHeader.getPreferredSize().height; + scroll.getViewport().addChangeListener(e -> invalidateCaches()); + + } + + /** + * Sets if the display should be compact or expanded. + * @param b if true, use compact format + */ + public void setCompactFormat(boolean b) { + if (!isExpandable()) { + return; + } + dataColumnModel = b ? new CompactDataColumnModel() : new ExpandedDataColumnModel(); + gTrable.setColumnModel(dataColumnModel); + userSize = null; + updateShape(); + } + + /** + * Associates an outgoing vertex with the component path of the internal data element + * within this vertex that connects to that external vertex. This is used to draw the + * outgoing edge at the same y offset where the referring data is displayed in the scrollable + * display area. + * @param end the external vertex we are associating with a component path + * @param componentPath the component path of the data whose reference generated the external + * vertex. + */ + public void addOutgoingEdge(EgVertex end, int[] componentPath) { + cleanUpDeletedEdges(); + outgoingEdgeOffsetMap.put(end, new OutgoingEdgeOffsetInfo(componentPath)); + cachedOutgoingOffsetsValid = false; + } + + @Override + public String getToolTipText(MouseEvent event) { + Component source = event.getComponent(); + if (SwingUtilities.isDescendingFrom(source, genericHeader)) { + if (!(source instanceof JComponent)) { + return null; + } + JComponent jComponent = (JComponent) source; + return jComponent.getToolTipText(); + } + return null; + } + + /** + * Associates an incoming vertex with the Address of the internal data element + * within this vertex that the external vertex refers to. This is used to draw the + * incoming edge at the same y offset where the referred to data is displayed in the scrollable + * display area. We only record this information for references that are not to the vertex's + * base address. References to the base address are always drawn to point at top left corner, + * but references directly to sub-data elements are drawn to point to the sub-element so we + * need to track where those y-offsets are located for those sub-data elements. + * + * @param start the external vertex we are associating with an offcut address in the vertex + * @param address the offcut address that the external vertex refers to + */ + public void addIncommingEdge(EgVertex start, Address address) { + cleanUpDeletedEdges(); + // only need to track the offcut incoming edges + if (!address.equals(vertex.getAddress())) { + incomingEdgeOffsetMap.put(start, new IncomingEdgeOffsetInfo(address)); + cachedIncomingOffsetsValid = false; + } + } + + /** + * {@return the y offset from center of where to draw the incoming edge endpoint.} + * @param v the external vertex for an incoming edge + */ + public int getIncommingEdgeOffsetFromCenter(EgVertex v) { + if (!cachedIncomingOffsetsValid) { + computeIncomingEdgeOffset(); + } + IncomingEdgeOffsetInfo info = incomingEdgeOffsetMap.get(v); + if (info == null) { + // if not in map, then this edge is not offset, so return the top of the vertex + return -getSize().height / 2 + headerHeight / 2; + } + int yOffsetFromTop = info.yOffset; + return yOffsetFromTop - getSize().height / 2; + } + + /** + * {@return the y offset from center of where to draw the outgoing edge startpoint.} + * @param v the external vertex for an outgoing edge + */ + public int getOutgoingEdgeOffsetFromCenter(EgVertex v) { + if (!cachedOutgoingOffsetsValid) { + computeOutgoingEdgeOffsets(); + } + OutgoingEdgeOffsetInfo linkInfo = outgoingEdgeOffsetMap.get(v); + int yOffsetFromTop = (linkInfo != null ? linkInfo.yOffset : 0); + return yOffsetFromTop - getSize().height / 2; + } + + /** + * Adds an action to this vertex's header component. + * @param action the action to add + */ + public void addAction(DockingAction action) { + genericHeader.actionAdded(action); + genericHeader.update(); + headerHeight = genericHeader.getPreferredSize().height; + } + + /** + * Sets the size of this panel + * @param size the new size for this panel + */ + public void setSizeByUser(Dimension size) { + userSize = size; + updateShape(); + controller.repaint(); + } + + /** + * {@return the shape of this panel.} + */ + public Shape getShape() { + return preferredShape; + } + + /** + * Updates the cached shape of this panel + * @return true if the shape changed + */ + public boolean updateShape() { + gTrable.invalidate(); + Dimension preferredSize = scroll.getPreferredSize(); + int width = userSize != null ? userSize.width : preferredSize.width; + int height = userSize != null ? userSize.height : preferredSize.height + headerHeight; + if (!isOpen()) { + height = preferredSize.height + headerHeight; + } + if (preferredShape.width == width && preferredShape.height == height) { + return false; + } + preferredShape.width = width; + preferredShape.height = height; + + return true; + } + + /** + * Sets this vertex to be selected or not. + * @param selected if true the vertex is selected + */ + public void setSelected(boolean selected) { + genericHeader.setSelected(selected); + if (selected) { + navigate(gTrable.getSelectedRow()); + } + } + + /** + * Sets this vertex to be focused or not. + * @param focused if true the vertex is focused + */ + public void setFocused(boolean focused) { + if (focused) { + navigate(gTrable.getSelectedRow()); + } + } + + /** + * Dispose this component; + */ + public void dispose() { + vertex = null; + } + + /** + * {@return the amount the current scroll if offset from an even row boundary.} + */ + public int getScrollRowOffset() { + return gTrable.getRowOffcut(); + } + + /** + * Adds new vertices for all outgoing references from the given row. + * @param row the row containing the data object to get references and add outgoing vertices. + */ + public void openPointerReference(int row) { + DataRowObject dataDisplayRow = model.getRow(row); + if (dataDisplayRow.hasOutgoingReferences()) { + Data data = dataDisplayRow.getData(); + controller.addOutGoingReferences(vertex, data); + } + } + + /** + * {@return the height of the vertex header component.} + */ + public int getHeaderHeight() { + return headerHeight; + } + + /** + * Compares the the associated data component paths for the given external outgoing vertices. + * The vertices are ordered by the associated data paths for the given vertices. + * @param v1 vertex 1 + * @param v2 vertex 2 + * @return a negative integer, zero, or a positive integer as the + * first argument is less than, equal to, or greater than the + * second. + */ + public int comparePaths(DegVertex v1, DegVertex v2) { + // use the edge info to get the component paths since it is cheaper than getting + // it from the data objects which computes the path every time you ask it. + OutgoingEdgeOffsetInfo edgeInfo1 = outgoingEdgeOffsetMap.get(v1); + OutgoingEdgeOffsetInfo edgeInfo2 = outgoingEdgeOffsetMap.get(v2); + return pathComparator.compare(edgeInfo1.componentPath, edgeInfo2.componentPath); + } + + /** + * Sets this vertex to be the overall graph original source vertex or not. Only one vertex + * in the graph should be set to true. + * @param b if true, this vertex will be set as the original source vertex (shows a icon + * in the header if it is the original source vertex.) + */ + public void setIsRoot(boolean b) { + if (b) { + genericHeader.setIcon(Icons.HOME_ICON); + } + else { + genericHeader.setIcon(null); + } + } + + /** + * Causes the header to relayout it components. Usually called after actions are added or + * removed. + */ + public void updateHeader() { + genericHeader.update(); + } + + /** + * Expands the given row and its child rows recursively. + * @param rowIndex the index to expand. A value of 0 will expand all possible rows. + */ + public void expandRecursivley(int rowIndex) { + gTrable.expandRowRecursively(rowIndex); + } + + /** + * Fully expands all expandable rows. + */ + public void expandAll() { + gTrable.expandAll(); + } + + /** + * Collapses all rows. + */ + public void collapseAll() { + gTrable.collapseAll(); + } + + /** + * {@return true if the data being display is expandable.} + */ + public boolean isExpandable() { + return model.getRow(0).isExpandable(); + } + + /** + * Expands the given row. + * @param row the row to expand + */ + public void expand(int row) { + model.expandRow(row); + } + + /** + * Sets a new {@link Data} object for this panel. + * @param newData the new Data object to display in this panel + */ + public void setData(Data newData) { + boolean isFirstLevelOpen = model.isExpanded(0); + model.setData(newData); + if (isFirstLevelOpen) { + model.expandRow(0); + } + } + + /** + * {@return the title of this vertex.} + */ + public String getTitle() { + return genericHeader.getTitle(); + } + + /** + * {@return a list of all the DataRowObjects in this component.} + */ + public List getRowObjects() { + List list = new ArrayList<>(); + int n = model.getRowCount(); + for (int i = 0; i < n; i++) { + DataRowObject row = model.getRow(i); + list.add(row); + } + return list; + } + + /** + * {@return the action with the given name or null if the header has not action with that name.} + * @param name the name of the action to find + */ + public DockingActionIf getAction(String name) { + return genericHeader.getAction(name); + } + + private void cleanUpDeletedEdges() { + Set outgoingVertices = controller.getOutgoingVertices(vertex); + outgoingEdgeOffsetMap.keySet().retainAll(outgoingVertices); + + Set incomingVertices = controller.getIncomingVertices(vertex); + incomingEdgeOffsetMap.keySet().retainAll(incomingVertices); + + } + + private void invalidateCaches() { + cachedIncomingOffsetsValid = false; + cachedOutgoingOffsetsValid = false; + } + + private void buildComponent(Data data, boolean compact) { + model = new DataTrableRowModel(data); + if (!isExpandable()) { + // if we only ever have a top level row, then no point in being in expanded format + compact = true; + } + dataColumnModel = compact ? new CompactDataColumnModel() : new ExpandedDataColumnModel(); + model.expandRow(0); + gTrable = new GTrable<>(model, dataColumnModel); + gTrable.setPreferredVisibleRowCount(1, 15); + gTrable.addCellClickedListener(this::cellClicked); + model.addListener(this::modelDataChanged); + gTrable.addSelectedRowConsumer(this::selectedRowChanged); + + scroll = new JScrollPane(gTrable); + scroll.getViewport().addChangeListener(e -> controller.repaint()); + add(scroll, BorderLayout.CENTER); + + genericHeader = new GenericHeader(); + genericHeader.setComponent(scroll); + add(genericHeader, BorderLayout.NORTH); + + } + + private void modelDataChanged() { + if (updateShape()) { + controller.relayoutGraph(); + } + cachedOutgoingOffsetsValid = false; + cachedIncomingOffsetsValid = false; + controller.repaint(); + } + + private void computeIncomingEdgeOffset() { + cachedIncomingOffsetsValid = true; + if (incomingEdgeOffsetMap.isEmpty()) { + return; + } + int rowHeight = gTrable.getRowHeight(); + int rowOffset = gTrable.getRowOffcut(); + Dimension size = getSize(); + + List
visibleAddresses = getVisibleAddresses(); + Address minAddress = visibleAddresses.get(0); + Address maxAddress = visibleAddresses.get(visibleAddresses.size() - 1); + for (IncomingEdgeOffsetInfo info : incomingEdgeOffsetMap.values()) { + Address address = info.address; + if (address.compareTo(minAddress) < 0) { + info.yOffset = headerHeight; + continue; + } + if (address.compareTo(maxAddress) > 0) { + info.yOffset = size.height; + continue; + } + int index = getIndex(visibleAddresses, address); + int offset = index * rowHeight - rowOffset + rowHeight / 2 + headerHeight; + if (size.height > headerHeight) { + offset = Math.clamp(offset, headerHeight, size.height); + } + info.yOffset = offset; + } + + } + + private void computeOutgoingEdgeOffsets() { + cachedOutgoingOffsetsValid = true; + int rowHeight = gTrable.getRowHeight(); + int rowOffset = gTrable.getRowOffcut(); + Dimension size = getSize(); + + List paths = getVisibleDataPaths(); + int[] minPath = paths.get(0); + for (OutgoingEdgeOffsetInfo info : outgoingEdgeOffsetMap.values()) { + int[] path = info.componentPath; + if (pathComparator.compare(path, minPath) < 0) { + info.yOffset = headerHeight; + continue; + } + int index = getIndex(paths, path); + int offset = index * rowHeight - rowOffset + rowHeight / 2 + headerHeight; + if (size.height > headerHeight) { + offset = Math.clamp(offset, headerHeight, size.height); + } + info.yOffset = offset; + } + } + + private int getIndex(List paths, int[] componentPath) { + int index = Collections.binarySearch(paths, componentPath, pathComparator); + + if (index < 0) { + index = -index - 2; + } + return index; + } + + private int getIndex(List
addresses, Address address) { + int index = Collections.binarySearch(addresses, address); + + // We have already checked that the path is > the first row displayed and less than + // the last row we displayed. Therefore, it the binary search doesn't find a direct hit, + // it means the desired row is currently in a parent that is not expanded, so we want + // the offset to be the parent. Normally, the convention for binary search is to do + // -index-1 to get the location where the value would be inserted. But we want the parent, + // which is back one more, so we subtract 2 instead of 1. + + if (index < 0) { + index = -index - 2; + } + + // get the bottom most address (lowest level component) if more than one row have + // the same address + while (index < addresses.size() - 1) { + if (!addresses.get(index + 1).equals(address)) { + break; + } + index++; + } + return index; + } + + private void updateTitle() { + Data data = model.getData(); + String title = "@ " + data.getAddressString(false, false); + String label = data.getLabel(); + if (label != null) { + title = label + " " + title; + } + genericHeader.setTitle(title); + } + + private boolean isOpen() { + return model.getRow(0).isExpanded(); + } + + private class DataVertexKeyListener implements KeyListener { + + @Override + public void keyTyped(KeyEvent e) { + KeyboardFocusManager kfm = + KeyboardFocusManager.getCurrentKeyboardFocusManager(); + kfm.redispatchEvent(gTrable, e); + e.consume(); // consume all events; signal that our text area will handle them + } + + @Override + public void keyReleased(KeyEvent e) { + KeyboardFocusManager kfm = + KeyboardFocusManager.getCurrentKeyboardFocusManager(); + kfm.redispatchEvent(gTrable, e); + e.consume(); // consume all events; signal that our text area will handle them + } + + @Override + public void keyPressed(KeyEvent e) { + KeyboardFocusManager kfm = + KeyboardFocusManager.getCurrentKeyboardFocusManager(); + kfm.redispatchEvent(gTrable, e); + e.consume(); // consume all events; signal that our text area will handle them + } + + } + + private List
getVisibleAddresses() { + Range visibleRows = gTrable.getVisibleRows(); + List
visiblePaths = new ArrayList<>((int) visibleRows.size()); + for (int i = visibleRows.min; i <= visibleRows.max; i++) { + DataRowObject displayRow = model.getRow(i); + Data data = displayRow.getData(); + visiblePaths.add(data.getAddress()); + } + return visiblePaths; + + } + + private List getVisibleDataPaths() { + Range visibleRows = gTrable.getVisibleRows(); + List visiblePaths = new ArrayList<>((int) visibleRows.size()); + for (int i = visibleRows.min; i <= visibleRows.max; i++) { + DataRowObject displayRow = model.getRow(i); + Data data = displayRow.getData(); + visiblePaths.add(data.getComponentPath()); + } + return visiblePaths; + } + + private void cellClicked(int row, int column, MouseEvent ev) { + if (isPointerButtonColumn(column)) { + openPointerReference(row); + } + } + + private boolean isPointerButtonColumn(int column) { + boolean isCompact = dataColumnModel instanceof CompactDataColumnModel; + int pointerColumn = isCompact ? 2 : 3; + return column == pointerColumn; + } + + private void selectedRowChanged(int row) { + controller.repaint(); + navigate(row); + } + + private void navigate(int row) { + if (row < 0) { + row = 0; // if now row selected, use the first row to navigate + } + DataRowObject dataDisplayRow = model.getRow(row); + Data data = dataDisplayRow.getData(); + controller.navigateOut(data.getAddress(), data.getComponentPath()); + + } + + public boolean isSelectedRowExpandable() { + int row = gTrable.getSelectedRow(); + if (row < 0) { + return false; + } + return model.isExpandable(row); + } + + public void expandSelectedRowRecursively() { + int row = gTrable.getSelectedRow(); + if (row >= 0) { + gTrable.expandRowRecursively(row); + } + } + + private static class OutgoingEdgeOffsetInfo { + public int[] componentPath; + public int yOffset; + + OutgoingEdgeOffsetInfo(int[] componentPath) { + this.componentPath = componentPath; + yOffset = 0; + } + } + + private static class IncomingEdgeOffsetInfo { + public Address address; + public int yOffset; + + IncomingEdgeOffsetInfo(Address toAddress) { + this.address = toAddress; + } + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DtComponentPathComparator.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DtComponentPathComparator.java new file mode 100644 index 0000000000..fd22fd0fdd --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/DtComponentPathComparator.java @@ -0,0 +1,43 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package datagraph.data.graph.panel; + +import java.util.Comparator; + +/** + * Comparator for comparing two data component paths + */ +public class DtComponentPathComparator implements Comparator { + @Override + public int compare(int[] o1, int[] o2) { + int level = 0; + int length1 = o1.length; + int length2 = o2.length; + while (level < length1 && level < length2) { + int index1 = o1[level]; + int index2 = o2[level]; + if (index1 != index2) { + return index1 - index2; + } + level++; + } + if (length1 == length2) { + return 0; + } + return length1 - length2; + + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/CompactDataColumnModel.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/CompactDataColumnModel.java new file mode 100644 index 0000000000..09439f4b6b --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/CompactDataColumnModel.java @@ -0,0 +1,132 @@ +/* ### + * 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 datagraph.data.graph.panel.model.column; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import datagraph.data.graph.panel.model.row.DataRowObject; +import docking.widgets.trable.*; + +/** + * A GTrable column model for showing information about data and sub-data items. This model + * shows the information in a compact format generally showing the field name and the current + * value for that field. For the top element that has no field name, the datatype name is shown + * instead. Also for the value field, if it doesn't have a value because it has sub pieces that + * have values (structure), then the datatype name is shown in the value value field. + */ +public class CompactDataColumnModel extends GTrableColumnModel { + + @Override + protected void populateColumns(List> columnList) { + columnList.add(new NameColumn()); + columnList.add(new ValueColumn()); + columnList.add(new PointerButtonColumn()); + } + + /** + * {@return true if the given column represents the pointer icon column where the user can + * click to add new vertices to the graph.} + * @param column the column to check + */ + public boolean isPointerButtonColumn(int column) { + return column == 2; + } + + private class NameColumn extends GTrableColumn { + @Override + public String getValue(DataRowObject row) { + // if in compact format, show type in name field if it doesn't have a name + if (row.getIndentLevel() == 0) { + return row.getDataType(); + } + return row.getName(); + } + + @Override + protected int getPreferredWidth() { + return 150; + } + + } + + private static class TypeColumn extends GTrableColumn { + @Override + public String getValue(DataRowObject row) { + return row.getDataType(); + } + + @Override + protected int getPreferredWidth() { + return 120; + } + } + + private class ValueColumn extends GTrableColumn { + private GTrableCellRenderer renderer = new ValueColumnRenderer(); + + @Override + public String getValue(DataRowObject row) { + String value = row.getValue(); + if (!StringUtils.isBlank(value)) { + value = " = " + value; + } + else if (row.getIndentLevel() > 0) { + // special case if in compact for to show data types in the value column + // on entries that can be opened to show inner values. + value = " " + row.getDataType(); + } + return value; + } + + @Override + protected int getPreferredWidth() { + return 100; + } + + @Override + public GTrableCellRenderer getRenderer() { + return renderer; + } + + } + + private static class PointerButtonColumn extends GTrableColumn { + private GTrableCellRenderer renderer = new PointerColumnRenderer(); + + @Override + public Boolean getValue(DataRowObject row) { + return row.hasOutgoingReferences(); + } + + @Override + protected int getPreferredWidth() { + return 24; + } + + @Override + public boolean isResizable() { + return false; + } + + @Override + public GTrableCellRenderer getRenderer() { + return renderer; + } + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ExpandedDataColumnModel.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ExpandedDataColumnModel.java new file mode 100644 index 0000000000..772676f2ce --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ExpandedDataColumnModel.java @@ -0,0 +1,122 @@ +/* ### + * 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 datagraph.data.graph.panel.model.column; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import datagraph.data.graph.panel.model.row.DataRowObject; +import docking.widgets.trable.*; + +/** + * A GTrable column model for showing information about data and sub-data items. This model + * shows the information in a expanded format that displays the datatype, the field name and + * the current value for that field. + */ +public class ExpandedDataColumnModel extends GTrableColumnModel { + + @Override + protected void populateColumns(List> columnList) { + columnList.add(new TypeColumn()); + columnList.add(new NameColumn()); + columnList.add(new ValueColumn()); + columnList.add(new PointerButtonColumn()); + } + + /** + * {@return true if the given column represents the pointer icon column where the user can + * click to add new vertices to the graph.} + * @param column the column to check + */ + public boolean isPointerButtonColumn(int column) { + return column == 3; + } + + private static class NameColumn extends GTrableColumn { + @Override + public String getValue(DataRowObject row) { + return row.getName(); + } + + @Override + protected int getPreferredWidth() { + return 150; + } + + } + + private static class TypeColumn extends GTrableColumn { + @Override + public String getValue(DataRowObject row) { + return row.getDataType(); + } + + @Override + protected int getPreferredWidth() { + return 120; + } + } + + private static class ValueColumn extends GTrableColumn { + private GTrableCellRenderer renderer = new ValueColumnRenderer(); + + @Override + public String getValue(DataRowObject row) { + String value = row.getValue(); + if (!StringUtils.isBlank(value)) { + value = " = " + value; + } + return value; + } + + @Override + protected int getPreferredWidth() { + return 100; + } + + @Override + public GTrableCellRenderer getRenderer() { + return renderer; + } + + } + + private static class PointerButtonColumn extends GTrableColumn { + private GTrableCellRenderer renderer = new PointerColumnRenderer(); + + @Override + public Boolean getValue(DataRowObject row) { + return row.hasOutgoingReferences(); + } + + @Override + protected int getPreferredWidth() { + return 24; + } + + @Override + public boolean isResizable() { + return false; + } + + @Override + public GTrableCellRenderer getRenderer() { + return renderer; + } + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/PointerColumnRenderer.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/PointerColumnRenderer.java new file mode 100644 index 0000000000..9cee9a3cbd --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/PointerColumnRenderer.java @@ -0,0 +1,44 @@ +/* ### + * 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 datagraph.data.graph.panel.model.column; + +import java.awt.Component; + +import javax.swing.Icon; + +import docking.widgets.trable.DefaultGTrableCellRenderer; +import docking.widgets.trable.GTrable; +import resources.Icons; + +/** + * Renderer for the pointer icon column where the use can click to add vertices to the graph. + */ +public class PointerColumnRenderer extends DefaultGTrableCellRenderer { + + private static final Icon ICON = Icons.RIGHT_ICON; + + @Override + public Component getCellRenderer(GTrable trable, Boolean value, boolean isSelected, + boolean hasFocus, int row, int column) { + + super.getCellRenderer(trable, null, isSelected, hasFocus, row, column); + + boolean isPointer = value; + Icon icon = isPointer ? ICON : null; + setIcon(icon); + return this; + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ValueColumnRenderer.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ValueColumnRenderer.java new file mode 100644 index 0000000000..e77b36cd66 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/column/ValueColumnRenderer.java @@ -0,0 +1,41 @@ +/* ### + * 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 datagraph.data.graph.panel.model.column; + +import java.awt.Color; +import java.awt.Component; + +import docking.widgets.trable.DefaultGTrableCellRenderer; +import docking.widgets.trable.GTrable; +import generic.theme.GColor; + +/** + * Column renderer for the values column. Used to change the foreground color for values. + */ +public class ValueColumnRenderer extends DefaultGTrableCellRenderer { + private Color valueColor = new GColor("color.fg.datagraph.value"); + + @Override + public Component getCellRenderer(GTrable trable, String value, boolean isSelected, + boolean hasFocus, int row, int column) { + + super.getCellRenderer(trable, value, isSelected, hasFocus, row, column); + if (value.startsWith(" =") && !isSelected) { + setForeground(valueColor); + } + return this; + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ArrayGroupDataRowObject.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ArrayGroupDataRowObject.java new file mode 100644 index 0000000000..acf1f4d91a --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ArrayGroupDataRowObject.java @@ -0,0 +1,61 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import ghidra.program.model.listing.Data; + +/** + * DataRowObject for groups of array elements. Because arrays can be large they are recursively + * grouped. + */ +public class ArrayGroupDataRowObject extends DataRowObject { + + private String name; + private Data data; + + ArrayGroupDataRowObject(Data data, int startIndex, int length, int indentLevel, + boolean isOpen) { + super(indentLevel, isOpen); + this.data = data; + this.name = "[" + startIndex + " - " + (startIndex + length - 1) + "]"; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return ""; + } + + @Override + public String getDataType() { + return ""; + } + + @Override + public boolean isExpandable() { + return true; + } + + @Override + public Data getData() { + return data; + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ComponentDataRowObject.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ComponentDataRowObject.java new file mode 100644 index 0000000000..60cb8323fe --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/ComponentDataRowObject.java @@ -0,0 +1,65 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import ghidra.program.model.listing.Data; + +/** + * DataRowObject for actual DataComponents. This directly corresponds to a + * Data or sub Data object in a program. + */ +public class ComponentDataRowObject extends DataRowObject { + static final int ARRAY_GROUP_SIZE = 100; + protected Data data; + + public ComponentDataRowObject(int indentLevel, Data data, boolean isOpen) { + super(indentLevel, isOpen); + this.data = data; + } + + @Override + public boolean isExpandable() { + return data.getNumComponents() > 0; + } + + @Override + public Data getData() { + return data; + } + + @Override + public String getName() { + return data.getFieldName(); + } + + @Override + public String getValue() { + return data.getDefaultValueRepresentation(); + } + + @Override + public String getDataType() { + return data.getDataType().getDisplayName(); + } + + @Override + public boolean hasOutgoingReferences() { + if (data.isPointer()) { + return true; + } + return data.getProgram().getReferenceManager().hasReferencesFrom(data.getAddress()); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObject.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObject.java new file mode 100644 index 0000000000..676bffad13 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObject.java @@ -0,0 +1,85 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import docking.widgets.table.GTable; +import docking.widgets.table.RowObject; +import ghidra.program.model.listing.Data; + +/** + * Abstract class for displaying rows in a Data GTrable model. Similar to a {@link RowObject} in + * a {@link GTable}. The big difference is that each row maintains its indent level and whether + * or not is is expanded. GTrables are like tables, but with a tree like structure. Each row that is + * a child of another row has its indent level set to one more than its parent. The expanded flag is + * used to indicate if a given row has visible child rows showing or not. + */ + +public abstract class DataRowObject { + private int indentLevel; + private boolean isExpanded; + + /** + * Constructor + * @param indentLevel the indent level for this row object + * @param isExpanded true if this object has child rows that are being displayed + */ + protected DataRowObject(int indentLevel, boolean isExpanded) { + this.indentLevel = indentLevel; + this.isExpanded = isExpanded; + } + + public int getIndentLevel() { + return indentLevel; + } + + public boolean isExpanded() { + return isExpanded; + } + + /** + * {@return the name for this row. Typically this will be the field name, but it could be + * a descriptive title for a group such as array range.} + */ + public abstract String getName(); + + /** + * {@return the interpreted value for the data at this location.} + */ + public abstract String getValue(); + + /** + * {@return the name of the datatype at this location.} + */ + public abstract String getDataType(); + + /** + * {@return true if the row can produce child rows.} + */ + public abstract boolean isExpandable(); + + /** + * {@return true if this location represents a pointer or has outgoing references} + */ + public boolean hasOutgoingReferences() { + return false; + } + + /** + * @{return the Data object associated with this row.} + */ + public abstract Data getData(); + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObjectCache.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObjectCache.java new file mode 100644 index 0000000000..6263b65c8c --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataRowObjectCache.java @@ -0,0 +1,59 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cache for {@link DataRowObject}s. DataRowObjects are created as needed to conserve space. The + * visible rows are kept in this cache to avoid having to recreate them on each paint call. It uses + * a simple premise that paint calls will paint rows in order. So anytime a put occurs that is not + * one more than the previous call, the assumption is that the view was scrolled, so the cache + * is cleared and a new cache sequence is started. + */ +public class DataRowObjectCache { + private static final int MAX_CACHE_SIZE = 300; + List cachedRows = new ArrayList<>(); + int startIndex = 0; + + public boolean contains(int rowIndex) { + int cacheIndex = rowIndex - startIndex; + return cacheIndex >= 0 && cacheIndex < cachedRows.size(); + } + + public DataRowObject getDataRow(int rowIndex) { + return cachedRows.get(rowIndex - startIndex); + } + + public void putData(int rowIndex, DataRowObject row) { + // This cache expects data to be put in sequentially from some start row. The idea is + // to cache the rows that are currently in the scrolled view. So anytime we are putting + // in a row that is not the next expected row in sequence, throw away the cache and + // start over. + if (rowIndex != startIndex + cachedRows.size() || cachedRows.size() > MAX_CACHE_SIZE) { + clear(); + startIndex = rowIndex; + } + cachedRows.add(row); + } + + public void clear() { + startIndex = 0; + cachedRows.clear(); + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataTrableRowModel.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataTrableRowModel.java new file mode 100644 index 0000000000..80cab51b88 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/DataTrableRowModel.java @@ -0,0 +1,141 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import docking.widgets.trable.AbstractGTrableRowModel; +import docking.widgets.trable.GTrable; +import ghidra.program.model.listing.Data; + +/** + * Row model for Data objects in {@link GTrable}. Most of the complexity is handled by + * the {@link OpenDataChildren} object. If only the top most row is displaying (i.e. it is not + * expanded), then the openChildren object is null. When row 0 is expanded, an OpenChildren is + * created to manage the rows for the sub data child rows. This pattern is repeated inside the + * OpenChilren node (open rows have corresponding OpenChildren objects to manage its child + * rows.) + */ +public class DataTrableRowModel extends AbstractGTrableRowModel { + private Data data; + private OpenDataChildren openChildren; + private DataRowObjectCache cache = new DataRowObjectCache(); + + public DataTrableRowModel(Data data) { + this.data = data; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + cache.clear(); + openChildren = null; + } + + @Override + public int getRowCount() { + if (openChildren == null) { + return 1; + } + return openChildren.getRowCount() + 1; + } + + @Override + public DataRowObject getRow(int rowIndex) { + if (cache.contains(rowIndex)) { + return cache.getDataRow(rowIndex); + } + DataRowObject row = generateRow(rowIndex); + cache.putData(rowIndex, row); + return row; + } + + private DataRowObject generateRow(int rowIndex) { + if (rowIndex == 0) { + return new ComponentDataRowObject(0, data, openChildren != null); + } + return openChildren.getRow(rowIndex - 1); + } + + @Override + public boolean isExpandable(int rowIndex) { + DataRowObject dataRow = getRow(rowIndex); + return dataRow != null && dataRow.isExpandable(); + } + + @Override + public boolean isExpanded(int rowIndex) { + DataRowObject row = getRow(rowIndex); + return row != null && row.isExpanded(); + } + + @Override + public int collapseRow(int rowIndex) { + cache.clear(); + if (rowIndex < 0 || rowIndex >= getRowCount()) { + throw new IndexOutOfBoundsException(); + } + if (rowIndex == 0) { + if (openChildren == null) { + return 0; + } + int diff = openChildren.getRowCount(); + openChildren = null; + fireModelChanged(); + return diff; + } + int rowCountDiff = openChildren.collapseChild(rowIndex - 1); + fireModelChanged(); + return rowCountDiff; + } + + @Override + public int expandRow(int rowIndex) { + cache.clear(); + if (rowIndex < 0 || rowIndex >= getRowCount()) { + throw new IndexOutOfBoundsException(); + } + if (rowIndex == 0) { + // are we already open? + if (openChildren != null) { + return 0; + } + openChildren = OpenDataChildren.createOpenDataNode(data, 0, 0, 1); + fireModelChanged(); + return openChildren.getRowCount(); + } + int diff = openChildren.expandChild(rowIndex - 1); + fireModelChanged(); + return diff; + } + + @Override + public int getIndentLevel(int rowIndex) { + DataRowObject row = getRow(rowIndex); + return row.getIndentLevel(); + } + + public void refresh() { + cache.clear(); + if (openChildren != null) { + if (!openChildren.refresh(data)) { + openChildren = null; + } + } + fireModelChanged(); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/OpenDataChildren.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/OpenDataChildren.java new file mode 100644 index 0000000000..843c1159ce --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/data/graph/panel/model/row/OpenDataChildren.java @@ -0,0 +1,469 @@ +/* ### + * 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 datagraph.data.graph.panel.model.row; + +import java.util.*; + +import ghidra.program.model.data.Array; +import ghidra.program.model.listing.Data; + +/** + * Manages open rows in a DataTrableModel. Since this manages the open rows for its parent data + * object, it has one row for each sub data component in its parent data. Of course, each of these + * rows can also potentially be expandable. If any of the top level rows managed by this object are + * expanded, it creates a OpenDataChildren object to manage its sub rows. The OpenChildrenObjects + * are store in a list that is ordered by its row index. + *

+ * To find out if a row is expanded or not, a binary search is used to find out if there is an + * OpenDataChildren object for that row. If it is expanded, that object is used to recursively go + * down to get the leaf row object. + */ +public abstract class OpenDataChildren implements Comparable { + // when arrays are bigger than 100, we group them into chunks of 100 each. The size + // should be a power of 10 and 100 seems like a good choice. + private static final int ARRAY_GROUP_SIZE = 100; + private int rowIndex; // row index relative to parent + private int rowCount; // number of rows represented by this node, including any open children + private int componentIndex; + private int componentCount; + protected int indentLevel; + protected List openChildren; + protected Data data; + + /** + * Constructor + * @param data the data object that this is managing rows for its child data components + * @param rowIndex the row index is the overall row index of the first child element row + * @param componentIndex the index of the the managed data component within its parent + * @param componentCount the number of direct rows this managed component has. (It may have + * indirect rows if its rows have child rows) + * @param indentLevel the indent level for all direct rows managed by this object + */ + protected OpenDataChildren(Data data, int rowIndex, int componentIndex, + int componentCount, int indentLevel) { + this.data = data; + this.rowIndex = rowIndex; + this.componentCount = componentCount; + this.rowCount = componentCount; + this.componentIndex = componentIndex; + this.indentLevel = indentLevel; + openChildren = new ArrayList<>(); + } + + /** + * Convenience static factory method for create the correct subtype of an OpenDataChildren + * object. (Arrays require special handling.) + * @param data the data object that is being expanded and this object will manage its child + * data components. + * @param rowIndex the overall row index of the first child row that is expanded + * @param componentIndex the component index of the data within its parent that this object + * is managing + * @param indentLevel the indent level for all rows directly managed by this object + * @return a new OpenDataChildren object + */ + public static OpenDataChildren createOpenDataNode(Data data, int rowIndex, int componentIndex, + int indentLevel) { + int numComponents = data.getNumComponents(); + if (data.getDataType() instanceof Array) { + if (numComponents <= ARRAY_GROUP_SIZE) { + return new ArrayElementsComponentNode(data, rowIndex, componentIndex, numComponents, + 0, indentLevel); + } + return new ArrayGroupComponentNode(data, rowIndex, componentIndex, 0, numComponents, + indentLevel); + } + return new DataComponentNode(data, rowIndex, componentIndex, numComponents, indentLevel); + + } + + /** + * Private constructor used to create a object that can be used in a binary search + * @param rowIndex the row to search for + */ + private OpenDataChildren(int rowIndex) { + this.rowIndex = rowIndex; + } + + /** + * {@return the total number of rows currently managed by this object (includes rows + * recursively managed by its expanded children)} + */ + public int getRowCount() { + return rowCount; + } + + /** + * {@return the row object for the given index.} + * @param childRowIndex the index to get a row for. This index is relative to this + * OpenDataChildren object and not the overall row index for the model. + */ + public DataRowObject getRow(int childRowIndex) { + + OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex); + if (node == null) { + return generateRow(childRowIndex, false); + } + + if (node.getRowIndex() == childRowIndex) { + return generateRow(node.getComponentIndex(), true); + } + + int childIndex = childRowIndex - node.getRowIndex() - 1; + if (childIndex < node.getRowCount()) { + return node.getRow(childIndex); + } + + int childComponentIndex = node.getComponentIndex() + childRowIndex - node.getRowIndex() - + node.getRowCount(); + return generateRow(childComponentIndex, false); + } + + protected int getComponentIndex() { + return componentIndex; + } + + protected abstract DataRowObject generateRow(int childComponentIndex, boolean isOpen); + + /** + * Expands the sub child at the given relative row index + * @param childRowIndex relative index to this OpenDataChildren object and not the overall + * model row index. + * @return the number of additional rows this caused to be added to the overall model + */ + public int expandChild(int childRowIndex) { + OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex); + if (node == null) { + return insertNode(childRowIndex, childRowIndex); + } + int indexPastNode = childRowIndex - node.getRowIndex(); + if (indexPastNode == 0) { + return 0; // we are already open + } + if (indexPastNode <= node.getRowCount()) { + int diff = node.expandChild(indexPastNode - 1); + rebuildNodeIndex(); + return diff; + } + int childComponentIndex = node.getComponentIndex() + indexPastNode - node.rowCount; + return insertNode(childRowIndex, childComponentIndex); + + } + + /** + * Collapse the child at the relative index. + * @param childRowIndex the relative child index to collapse + * @return the number of rows removed from the overall model + */ + public int collapseChild(int childRowIndex) { + OpenDataChildren node = findChildNodeAtOrBefore(childRowIndex); + + // the given index is not open, so do nothing + if (node == null) { + return 0; + } + + // compute the number of indexes past the open node we found + int offsetIndex = childRowIndex - node.rowIndex; + + // if we found a node at that index, just delete it since we only retain open nodes + if (offsetIndex == 0) { + openChildren.remove(node); + rebuildNodeIndex(); + return node.getRowCount(); + } + + // if the index is contained in the node, recurse down to close + if (offsetIndex <= node.getRowCount()) { + int diff = node.collapseChild(offsetIndex - 1); + rebuildNodeIndex(); + return diff; + } + + // again, the given index is not open, so do nothing + return 0; + } + + protected void rebuildNodeIndex() { + rowCount = componentCount; + + OpenDataChildren lastNode = null; + for (OpenDataChildren node : openChildren) { + if (lastNode == null) { + node.rowIndex = node.componentIndex; + } + else { + node.rowIndex = lastNode.rowIndex + node.componentIndex - lastNode.componentIndex + + lastNode.rowCount; + } + rowCount += node.rowCount; + lastNode = node; + } + } + + protected int insertNode(int childRowIndex, int childComponentIndex) { + OpenDataChildren newNode = generatedNode(childRowIndex, childComponentIndex); + if (newNode == null) { + return 0; // tried to open a node that can't be opened + } + int index = Collections.binarySearch(openChildren, newNode); + + // It should never be positive since we searched and didn't find one at this index + if (index >= 0) { + return 0; + } + int insertionIndex = -index - 1; + openChildren.add(insertionIndex, newNode); + rebuildNodeIndex(); + return newNode.getRowCount(); + } + + protected abstract OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex); + + /** + * Returns the rowIndex of of this node relative to its parent. + * @return the rowIndex of of this node relative to its parent + */ + int getRowIndex() { + return rowIndex; + } + + void setRowIndex(int rowIndex) { + this.rowIndex = rowIndex; + } + + @Override + public int compareTo(OpenDataChildren o) { + return getRowIndex() - o.getRowIndex(); + } + + public boolean refresh(Data newData) { + int newComponentCount = data.getNumComponents(); + + // if the data is different or it went to something not expandable (count == 0) + // return false to indicate to parent this node needs to be removed + if (data != newData || newComponentCount == 0) { + openChildren.clear(); + return false; + } + + // if the number of components changes, this node can remain, but need to close any + // children it has because it gets too complicated to correct + if (newComponentCount != componentCount) { + + openChildren.clear(); + componentCount = newComponentCount; + rowCount = componentCount; + return true; + } + + // recursively refresh open children, removing any that can't be refreshed + Iterator it = openChildren.iterator(); + while (it.hasNext()) { + OpenDataChildren child = it.next(); + if (!child.refresh(data.getComponent(child.componentIndex))) { + it.remove(); + } + } + rebuildNodeIndex(); + return true; + } + + protected OpenDataChildren findChildNodeAtOrBefore(int childRowIndex) { + if (openChildren.isEmpty()) { + return null; + } + int index = Collections.binarySearch(openChildren, new SearchKeyNode(childRowIndex)); + if (index < 0) { + index = -index - 2; + } + return index < 0 ? null : openChildren.get(index); + } + + private static class SearchKeyNode extends OpenDataChildren { + SearchKeyNode(int rowIndex) { + super(rowIndex); + } + + @Override + protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) { + return null; + } + + @Override + protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) { + return null; + } + } + + private static class DataComponentNode extends OpenDataChildren { + + public DataComponentNode(Data data, int rowIndex, int componentIndex, + int numComponents, int indentLevel) { + super(data, rowIndex, componentIndex, numComponents, indentLevel); + } + + @Override + protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) { + Data component = data.getComponent(childComponentIndex); + return new ComponentDataRowObject(indentLevel, component, isOpen); + } + + @Override + protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) { + Data component = data.getComponent(childComponentIndex); + return createOpenDataNode(component, childRowIndex, childComponentIndex, + indentLevel + 1); + } + + } + + private static class ArrayElementsComponentNode extends OpenDataChildren { + + private int arrayStartIndex; + private int totalArraySize; + + protected ArrayElementsComponentNode(Data data, int rowIndex, int componentIndex, + int componentCount, int arrayStartIndex, int indentLevel) { + super(data, rowIndex, componentIndex, componentCount, indentLevel); + this.arrayStartIndex = arrayStartIndex; + this.totalArraySize = data.getNumComponents(); + } + + @Override + protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) { + Data component = data.getComponent(arrayStartIndex + childComponentIndex); + return new ComponentDataRowObject(indentLevel + 1, component, isOpen); + } + + @Override + protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) { + Data component = data.getComponent(arrayStartIndex + childComponentIndex); + return createOpenDataNode(component, childRowIndex, childComponentIndex, + indentLevel + 1); + } + + @Override + public boolean refresh(Data newData) { + // NOTE: if this is a child of a array group node, this these check that can return + // false can't happen as they have already been checked by the parent node. These + // exist in case this is directly parented from a normal data node + if (!(newData.getDataType() instanceof Array)) { + return false; + } + int newTotalArraySize = data.getNumComponents(); + + // if the data is different or the array changed size, it needs to be removed + if (data != newData || newTotalArraySize != totalArraySize) { + return false; + } + + // recursively refresh open children. Since the elements are all the same type, if + // one can't refresh, then none can refresh + for (OpenDataChildren child : openChildren) { + Data component = data.getComponent(arrayStartIndex + child.getComponentIndex()); + if (!child.refresh(component)) { + openChildren.clear(); + break; + } + } + rebuildNodeIndex(); + return true; + } + } + + private static class ArrayGroupComponentNode extends OpenDataChildren { + + private int arrayStartIndex; + private int groupSize; + private int arrayCount; + private int totalArraySize; + + public ArrayGroupComponentNode(Data data, int rowIndex, int componentIndex, + int arrayStartIndex, int arrayCount, int indentLevel) { + super(data, rowIndex, componentIndex, getGroupCount(arrayCount), indentLevel); + this.arrayStartIndex = arrayStartIndex; + this.arrayCount = arrayCount; + this.groupSize = getGroupSize(arrayCount); + this.totalArraySize = data.getNumComponents(); + } + + private static int getGroupCount(int length) { + int groupSize = ARRAY_GROUP_SIZE; + int numGroups = (length + groupSize - 1) / groupSize; + while (numGroups > ARRAY_GROUP_SIZE) { + groupSize = groupSize * ARRAY_GROUP_SIZE; + numGroups = (length + groupSize - 1) / groupSize; + } + return numGroups; + } + + private static int getGroupSize(int length) { + int groupSize = ARRAY_GROUP_SIZE; + int numGroups = (length + groupSize - 1) / groupSize; + while (numGroups > ARRAY_GROUP_SIZE) { + groupSize = groupSize * ARRAY_GROUP_SIZE; + numGroups = (length + groupSize - 1) / groupSize; + } + return groupSize; + } + + @Override + protected DataRowObject generateRow(int childComponentIndex, boolean isOpen) { + int subArrayStartIndex = arrayStartIndex + childComponentIndex * groupSize; + int length = Math.min(groupSize, arrayCount - (childComponentIndex * groupSize)); + return new ArrayGroupDataRowObject(data, subArrayStartIndex, length, indentLevel + 1, + isOpen); + } + + @Override + protected OpenDataChildren generatedNode(int childRowIndex, int childComponentIndex) { + int arrayOffsetFromStart = childComponentIndex * groupSize; + int subArrayStartIndex = arrayStartIndex + arrayOffsetFromStart; + int length = Math.min(groupSize, arrayCount - arrayOffsetFromStart); + if (groupSize == ARRAY_GROUP_SIZE) { + return new ArrayElementsComponentNode(data, childRowIndex, childComponentIndex, + length, subArrayStartIndex, indentLevel + 1); + } + return new ArrayGroupComponentNode(data, childRowIndex, childComponentIndex, + subArrayStartIndex, length, indentLevel + 1); + } + + @Override + public boolean refresh(Data newData) { + if (!(newData.getDataType() instanceof Array)) { + return false; + } + int newTotalArraySize = data.getNumComponents(); + + // if the data is different or the array changed size, + // it needs to be removed + if (data != newData || newTotalArraySize != totalArraySize) { + return false; + } + + // recursively refresh open children. Since the elements are all the same type, if + // one can't refresh, then none can refresh so clear all open children + for (OpenDataChildren child : openChildren) { + if (!child.refresh(data)) { + openChildren.clear(); + break; + } + } + rebuildNodeIndex(); + return true; + } + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/AbstractExplorationGraph.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/AbstractExplorationGraph.java new file mode 100644 index 0000000000..0269fa0dae --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/AbstractExplorationGraph.java @@ -0,0 +1,143 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.util.*; + +import ghidra.graph.graphs.DefaultVisualGraph; +import ghidra.graph.viewer.layout.VisualGraphLayout; + +/** + * Base graph for exploration graphs. An exploration graph is a graph that typically starts with + * just one vertex, but then is interactively expanded with additional vertices by exploring + * incoming and outgoing links. + * + * @param the vertex type + * @param the edge type + */ +public abstract class AbstractExplorationGraph> + extends DefaultVisualGraph { + + private V root; + private EgGraphLayout layout; + + /** + * Constructor + * @param root the initial vertex in the graph. All nodes in the graph must be connected directly + * or indirectly to this vertex. + */ + public AbstractExplorationGraph(V root) { + this.root = root; + addVertex(root); + } + + /** + * {@return the original source vertex for this graph.} + */ + public V getRoot() { + return root; + } + + public void setLayout(EgGraphLayout layout) { + this.layout = layout; + } + + @Override + public VisualGraphLayout getLayout() { + return layout; + } + + /** + * {@return a set of all vertices that can trace a source path back to the given vertex. In + * other words either getSource() or getSource().getSource(), and so on, on the given vertex.} + * @param source the vertex to see if a vertex is a descendant from. + */ + public Set getDescendants(V source) { + Set descendents = new HashSet<>(); + getDescendants(source, descendents); + return descendents; + } + + /** + * Sets the root of this graph to the new root. + * @param newRoot the new source root for the graph + */ + public void setRoot(V newRoot) { + + // First clear out the source from each vertex to so they can be reassigned as we + // explore from the new root. We will use a null source to indicate the vertex hasn't been + // processed. + getVertices().forEach(v -> v.setSource(null)); + + // temporarily set the root source to mark it as processed + newRoot.setSource(newRoot); + + // create a queue of vertices to process and prime with the new root + Queue vertexQueue = new LinkedList<>(); + vertexQueue.add(newRoot); + + // follow edges assigning new source vertices + assignSource(vertexQueue); + + // set the root source to null to indicate it is the root source vertex. + newRoot.setSource(null); + + this.root = newRoot; + } + + private void getDescendants(V source, Set descendents) { + for (E e : getOutEdges(source)) { + V end = e.getEnd(); + if (source.equals(end.source)) { + descendents.add(end); + getDescendants(end, descendents); + } + } + for (E e : getInEdges(source)) { + V start = e.getStart(); + if (source.equals(start.source)) { + descendents.add(start); + getDescendants(start, descendents); + } + } + } + + private void assignSource(Queue vertexQueue) { + while (!vertexQueue.isEmpty()) { + V remove = vertexQueue.remove(); + processEdges(remove, vertexQueue); + } + } + + private void processEdges(V v, Queue vertexQueue) { + Collection outEdges = getOutEdges(v); + for (EgEdge edge : outEdges) { + V next = edge.getEnd(); + if (next.getSourceVertex() == null) { + next.setSource(v); + vertexQueue.add(next); + } + } + Collection inEdges = getInEdges(v); + for (E e : inEdges) { + V previous = e.getStart(); + if (previous.getSourceVertex() == null) { + previous.setSource(v); + vertexQueue.add(previous); + } + } + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdge.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdge.java new file mode 100644 index 0000000000..2d13492183 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdge.java @@ -0,0 +1,30 @@ +/* ### + * 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 datagraph.graph.explore; + +import ghidra.graph.viewer.edge.AbstractVisualEdge; + +/** + * An edge for the {@link AbstractExplorationGraph} + * @param The vertex type + */ +public abstract class EgEdge extends AbstractVisualEdge { + + public EgEdge(V start, V end) { + super(start, end); + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeRenderer.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeRenderer.java new file mode 100644 index 0000000000..1259aeb661 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeRenderer.java @@ -0,0 +1,49 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; + +import com.google.common.base.Function; + +import edu.uci.ics.jung.graph.Graph; +import edu.uci.ics.jung.visualization.RenderContext; +import ghidra.graph.viewer.VisualEdge; +import ghidra.graph.viewer.edge.VisualEdgeRenderer; + +/** + * Edge renderer for {@link AbstractExplorationGraph}s. Using information from the vertices to + * vertically align incoming and outgoing edges with the corresponding inner pieces in the + * vertex's display component. + * + * @param the vertex type + * @param the edge type + */ +public class EgEdgeRenderer> + extends VisualEdgeRenderer { + + @Override + public Shape getEdgeShape(RenderContext rc, Graph graph, E e, float x1, float y1, + float x2, float y2, boolean isLoop, Shape vertexShape) { + Function edgeXform = rc.getEdgeShapeTransformer(); + Shape shape = edgeXform.apply(e); + AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1); + + // apply the transformations; converting the given shape from model space into graph space + return xform.createTransformedShape(shape); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeTransformer.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeTransformer.java new file mode 100644 index 0000000000..3af22eced7 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgEdgeTransformer.java @@ -0,0 +1,88 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.awt.Dimension; +import java.awt.Shape; +import java.awt.geom.*; + +import com.google.common.base.Function; + +import ghidra.graph.viewer.GraphViewerUtils; +import ghidra.graph.viewer.VisualEdge; + +/** + * An edge shape that draws edges from left side of source vertex to the right side of the + * destination vertex. The vertical position on edges of the source vertex and destination is + * determined by the calls to the vertex so that edges can be aligned with a vertex's internals. + * + * @param the vertex type + * @param the edge type + */ +public class EgEdgeTransformer> + implements Function { + private static final double OVERLAP_GAP = 20; + private static int LOOP_SIZE = 12; + + /** + * Get the shape for this edge + * + * @param e the edge + * @return the edge shape + */ + @Override + public Shape apply(E e) { + V start = e.getStart(); + V end = e.getEnd(); + Dimension startSize = start.getComponent().getSize(); + + Point2D location = start.getLocation(); + double originX = location.getX(); + double originY = location.getY(); + + Point2D startPoint = start.getStartingEdgePoint(end); + Point2D endPoint = end.getEndingEdgePoint(start); + + boolean isLoop = start.equals(end); + + if (isLoop) { + Shape hollowEgdeLoop = GraphViewerUtils.createHollowEgdeLoop(); + AffineTransform xform = + AffineTransform.getTranslateInstance(startSize.width / 2 + LOOP_SIZE / 2, + start.getOutgoingEdgeOffsetFromCenter(end)); + + xform.scale(LOOP_SIZE, LOOP_SIZE); + return xform.createTransformedShape(hollowEgdeLoop); + } + GeneralPath path = new GeneralPath(); + + path.moveTo(startPoint.getX() - originX, startPoint.getY() - originY); + if (startPoint.getX() > endPoint.getX() - OVERLAP_GAP) { + if (start.getLocation().getX() != startPoint.getX()) { + path.lineTo(startPoint.getX() - originX + OVERLAP_GAP, startPoint.getY() - originY); + } + if (end.getLocation().getX() != endPoint.getX()) { + path.lineTo(endPoint.getX() - originX - OVERLAP_GAP, endPoint.getY() - originY); + } + } + + path.lineTo(endPoint.getX() - originX, endPoint.getY() - originY); + + return path; + + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgGraphLayout.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgGraphLayout.java new file mode 100644 index 0000000000..203810fc97 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgGraphLayout.java @@ -0,0 +1,268 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.util.*; +import java.util.function.Function; + +import javax.help.UnsupportedOperationException; + +import edu.uci.ics.jung.visualization.RenderContext; +import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; +import ghidra.graph.VisualGraph; +import ghidra.graph.viewer.layout.*; +import ghidra.graph.viewer.layout.LayoutListener.ChangeType; +import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; +import ghidra.util.task.TaskMonitor; + +/** + * A custom layout to arrange the vertices of an {@link AbstractExplorationGraph} using a tree + * structure that reflects the exploration order of the vertices. The basic algorithm is that + * an original vertex is the root vertex and all other vertices are displayed in a double tree + * structure that show a tree of outgoing edges to the right and a tree of incoming edges + * to the left. The immediate vertices at each level are simply shown in a vertical column. + *

+ * However, the tricky concept is that each vertex can then have edges that go back. For example, + * if the original vertex has three outgoing vertices, but then those + * vertices can spawn other incoming vertices. In this case, the vertex is pushed further out to + * make room for its spawned incoming vertices. This is done recursively, where each child + * vertex's sub tree is computed and then all the child subtrees are stacked in a column. + * + * @param the vertex type + * @param the edge type + */ +public abstract class EgGraphLayout> + extends AbstractVisualGraphLayout { + + private EgEdgeRenderer edgeRenderer = new EgEdgeRenderer<>(); + private EgEdgeTransformer edgeTransformer; + private Function vertexShapeTransformer = new VisualGraphVertexShapeTransformer(); + protected int verticalGap; + protected int horizontalGap; + + protected EgGraphLayout(AbstractExplorationGraph graph, String name, int verticalGap, + int horizontalGap) { + super(graph, name); + this.verticalGap = verticalGap; + this.horizontalGap = horizontalGap; + this.edgeTransformer = createEdgeTransformer(); + } + + protected abstract EgEdgeTransformer createEdgeTransformer(); + + protected abstract Comparator getIncommingVertexComparator(); + + protected abstract Comparator getOutgoingVertexComparator(); + + @Override + public Point2D apply(V v) { + if (v.hasUserChangedLocation()) { + return v.getLocation(); + } + return super.apply(v); + } + + @SuppressWarnings("unchecked") + @Override + public VisualGraph getVisualGraph() { + return (VisualGraph) getGraph(); + } + + @Override + public BasicEdgeRenderer getEdgeRenderer() { + return edgeRenderer; + } + + @Override + public com.google.common.base.Function getEdgeShapeTransformer( + RenderContext context) { + + return edgeTransformer; + } + + @Override + protected LayoutPositions doCalculateLocations(VisualGraph g, + TaskMonitor taskMonitor) { + if (!(g instanceof AbstractExplorationGraph layeredGraph)) { + throw new IllegalArgumentException("This layout only supports Layered graphs!"); + } + + try { + monitor = taskMonitor; + return computePositions(layeredGraph); + } + finally { + monitor = TaskMonitor.DUMMY; + } + + } + + private LayoutPositions computePositions(AbstractExplorationGraph g) { + GraphLocationMap locationMap = getLocationMap(g, g.getRoot()); + Map vertexLocations = locationMap.getVertexLocations(); + return LayoutPositions.createNewPositions(vertexLocations, + Collections.emptyMap()); + + } + + @Override + protected GridLocationMap performInitialGridLayout(VisualGraph g) { + // we override the method that calls this abstract method, so it isn't used. + throw new UnsupportedOperationException(); + } + + private GraphLocationMap getLocationMap(AbstractExplorationGraph g, V v) { + List> leftMaps = getMapsForIncommingEdges(g, v); + List> rightMaps = getMapsForOutgoingEdges(g, v); + + Shape shape = vertexShapeTransformer.apply(v); + Rectangle bounds = shape.getBounds(); + GraphLocationMap baseMap = new GraphLocationMap<>(v, bounds.width, bounds.height); + + if (leftMaps != null) { + mergeLeftMaps(baseMap, leftMaps); + } + if (rightMaps != null) { + mergeRightMaps(baseMap, rightMaps); + } + return baseMap; + } + + /** + * Merges all the incoming vertex sub-tree maps in a column to the left of the base map. Since + * these maps will all be to the left of their parent vertex base map, we align them in the + * column such that their right map edge boundaries align. + * @param baseMap the map for the parent vertex + * @param leftMaps the list of maps to be organized in a column to the left of the base map. + */ + private void mergeLeftMaps(GraphLocationMap baseMap, List> leftMaps) { + + int shiftY = getTopGroupShift(leftMaps, verticalGap); + int baseShiftX = baseMap.getMinX() - horizontalGap; + for (GraphLocationMap map : leftMaps) { + shiftY += map.getHeight() / 2; + int shiftX = baseShiftX - map.getMaxX(); + baseMap.merge(map, shiftX, shiftY); + shiftY += map.getHeight() / 2 + verticalGap; + } + } + + /** + * Merges all the outgoing vertex sub-tree maps in a column to the right of the base map. Since + * these maps will all be to the right of their parent vertex base map, we align them in the + * column such that their left map edge boundaries align. + * @param baseMap the map for the parent vertex + * @param rightMaps the list of maps to be organized in a column to the right of the base map. + */ + private void mergeRightMaps(GraphLocationMap baseMap, List> rightMaps) { + int shiftY = getTopGroupShift(rightMaps, verticalGap); + int baseShiftX = baseMap.getMaxX() + horizontalGap; + for (GraphLocationMap map : rightMaps) { + shiftY += map.getHeight() / 2; + int shiftX = baseShiftX - map.getMinX(); + baseMap.merge(map, shiftX, shiftY); + shiftY += map.getHeight() / 2 + verticalGap; + } + } + + private int getTopGroupShift(List> maps, int gap) { + int totalHeight = 0; + for (GraphLocationMap map : maps) { + totalHeight += map.getHeight(); + } + totalHeight += (maps.size() - 1) * gap; + return -totalHeight / 2; + } + + private List> getMapsForOutgoingEdges(AbstractExplorationGraph g, + V v) { + List edges = getOutgoingNextLayerEdges(g, v); + if (edges == null || edges.isEmpty()) { + return null; + } + return getOutgoingGraphMaps(g, edges); + } + + private List> getMapsForIncommingEdges(AbstractExplorationGraph g, + V v) { + List edges = getIncommingNextLayerEdges(g, v); + if (edges == null || edges.isEmpty()) { + return null; + } + return getIncomingGraphMaps(g, edges); + } + + private List> getOutgoingGraphMaps(AbstractExplorationGraph g, + List edges) { + List> maps = new ArrayList<>(edges.size()); + for (E e : edges) { + maps.add(getLocationMap(g, e.getEnd())); + } + return maps; + } + + private List> getIncomingGraphMaps(AbstractExplorationGraph g, + List edges) { + List> maps = new ArrayList<>(edges.size()); + for (E e : edges) { + maps.add(getLocationMap(g, e.getStart())); + } + return maps; + } + + private List getOutgoingNextLayerEdges(AbstractExplorationGraph g, V v) { + Collection outEdges = g.getOutEdges(v); + if (outEdges == null || outEdges.isEmpty()) { + return null; + } + List nextLayerEdges = new ArrayList<>(); + for (E e : outEdges) { + if (v.equals(e.getEnd().getSourceVertex())) { + nextLayerEdges.add(e); + } + } + Comparator c = getOutgoingVertexComparator(); + nextLayerEdges.sort((e1, e2) -> c.compare(e1.getEnd(), e2.getEnd())); + return nextLayerEdges; + } + + private List getIncommingNextLayerEdges(AbstractExplorationGraph g, V v) { + Collection inEdges = g.getInEdges(v); + if (inEdges == null || inEdges.isEmpty()) { + return null; + } + List nextLayerEdges = new ArrayList<>(); + for (E e : inEdges) { + if (v.equals(e.getStart().getSourceVertex())) { + nextLayerEdges.add(e); + } + } + Comparator c = getIncommingVertexComparator(); + nextLayerEdges.sort((e1, e2) -> c.compare(e1.getStart(), e2.getStart())); + return nextLayerEdges; + } + + @Override + protected void fireVertexLocationChanged(V v, Point2D p, ChangeType type) { + if (type == ChangeType.USER) { + v.setUserChangedLocation(new Point2D.Double(p.getX(), p.getY())); + } + super.fireVertexLocationChanged(v, p, type); + } +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgVertex.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgVertex.java new file mode 100644 index 0000000000..39337bf3e4 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/EgVertex.java @@ -0,0 +1,129 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.awt.geom.Point2D; + +import ghidra.graph.viewer.vertex.AbstractVisualVertex; + +/** + * A type of VisualVertex that is part of a exploration graph where each vertex (except the root + * vertex) has a concept of a source vertex that indicates what vertex's edge was first used to add + * this vertex to the graph. Any vertex in the graph can follow the source vertex and it's source + * vertex all the way back to the root source vertex. This means that any graph consisting of + * {@link EgVertex}s, regardless of its full set of edges, has a natural representation as a tree + * which is useful for creating a layout that reflects how the tree was generated from a single + * source seed vertex. + */ +public abstract class EgVertex extends AbstractVisualVertex { + protected EgVertex source; + private boolean userChangedLocation = false; + + /** + * Constructor + * @param source the vertex from which this vertex was added by following an edge + */ + public EgVertex(EgVertex source) { + this.source = source; + } + + /** + * {@return the vertex that was used to discover this vertex.} + */ + public EgVertex getSourceVertex() { + return source; + } + + @Override + public void dispose() { + // subclasses can override this if they need to perform cleanup + } + + @Override + public void setLocation(Point2D location) { + if (userChangedLocation) { + return; + } + super.setLocation(location); + } + + /** + * Manually sets the location for this vertex. + * @param location the location for the vertex + */ + public void setUserChangedLocation(Point2D location) { + userChangedLocation = true; + super.setLocation(location); + } + + /** + * Clears the user changed location lock, allowing the vertex to get a new location when the + * graph is laid out. + */ + public void clearUserChangedLocation() { + userChangedLocation = false; + } + + /** + * {@return true if the user manually set the location. A manually set location ignores + * locations generated by the layout.} + */ + public boolean hasUserChangedLocation() { + return userChangedLocation; + } + + /** + * {@return true if this is the root node for the exploration graph.} + */ + public boolean isRoot() { + return source == null; + } + + protected void setSource(EgVertex source) { + this.source = source; + } + + /** + * {@return the point to use for the first point in an edge going to the given end vertex.} + * @param end the destination vertex for the edge + */ + protected abstract Point2D getStartingEdgePoint(EgVertex end); + + /** + * {@return the point to use for the last point in an edge coming from the given start vertex.} + * @param start the starting vertex for the edge + */ + protected abstract Point2D getEndingEdgePoint(EgVertex start); + + /** + * {@return the outgoing offset of the edge from the center of the vertex for the edge ending + * at the given end vertex.} + * @param end the destination vertex of the edge to get an outgoing edge offset for + */ + protected int getOutgoingEdgeOffsetFromCenter(EgVertex end) { + return 0; + } + + /** + * {@return the incoming offset of the edge from the center of the vertex from the edge starting + * at the given start vertex.} + * @param start the starting vertex of the edge to get an incoming edge offset for + */ + protected int getIncomingEdgeOffsetFromCenter(EgVertex start) { + return 0; + } + +} diff --git a/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/GraphLocationMap.java b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/GraphLocationMap.java new file mode 100644 index 0000000000..0ce48ba0f5 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/main/java/datagraph/graph/explore/GraphLocationMap.java @@ -0,0 +1,130 @@ +/* ### + * 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 datagraph.graph.explore; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.*; + +/** + * Map of vertex locations in layout space and the boundaries for the space this sub graph + * occupies. + * + * @param the vertex type + */ +public class GraphLocationMap { + private Map vertexPoints = new HashMap<>(); + private int minX; + private int maxX; + private int minY; + private int maxY; + + /** + * Constructs a map with exactly one vertex which is located at P(0,0). The size of the vertex + * determines the map's bounds. + * @param vertex the initial vertex + * @param width the width of the the verte. + * @param height the height of the vertex + */ + public GraphLocationMap(V vertex, int width, int height) { + vertexPoints.put(vertex, new Point(0, 0)); + this.maxX = (width + 1) / 2; // add 1 so that odd sizes have the extra size to the right + this.minX = -width / 2; + this.maxY = (height + 1) / 2; // add 1 so that odd sizes have the extra size to the bottom + this.minY = -height / 2; + } + + /** + * Merges another {@link GraphLocationMap} into this one shifting its bounds and bounds by the + * given shift values. When this completes, the other map is disposed. + * @param other the other GridLocationMap to merge into this one + * @param xShift the amount to shift the other map horizontally before merging. + * @param yShift the amount to shift the other map vertically before merging. + */ + public void merge(GraphLocationMap other, int xShift, int yShift) { + other.shift(xShift, yShift); + vertexPoints.putAll(other.vertexPoints); + + this.minX = Math.min(minX, other.minX); + this.maxX = Math.max(maxX, other.maxX); + this.minY = Math.min(minY, other.minY); + this.maxY = Math.max(maxY, other.maxY); + other.dispose(); + } + + private void dispose() { + vertexPoints.clear(); + } + + private void shift(int xShift, int yShift) { + Collection values = vertexPoints.values(); + for (Point point : values) { + point.x += xShift; + point.y += yShift; + } + maxX += xShift; + minX += xShift; + maxY += yShift; + minY += yShift; + } + + /** + * {@return the width of this map. This includes space for the size of vertices and not just + * their center points.} + */ + public int getWidth() { + return maxX - minX; + } + + /** + * {@return the height of this map. This includes space for the size of vertices and not just + * their center points.} + */ + public int getHeight() { + return maxY - minY; + } + + /** + * {@return the location for the given vertex.} + * @param v the vertex to get a location for + */ + public Point get(V v) { + return vertexPoints.get(v); + } + + /** + * {@return a map of the vertices and their locations.} + */ + public Map getVertexLocations() { + Map points = new HashMap<>(); + points.putAll(vertexPoints); + return points; + } + + /** + * {@return the minimum x coordinate of the graph.} + */ + public int getMinX() { + return minX; + } + + /** + * {@return the maximum x coordinate of the graph.} + */ + public int getMaxX() { + return maxX; + } +} diff --git a/Ghidra/Features/DataGraph/src/main/resources/images/view_detailed_16.png b/Ghidra/Features/DataGraph/src/main/resources/images/view_detailed_16.png new file mode 100644 index 0000000000000000000000000000000000000000..89feaeba457fc4706fcb293436af5c40a7f0f153 GIT binary patch literal 961 zcmV;y13vtTP)S#@)vN0mNeCVJPEJ z6w31O8AQV`5dMQi1MyEN{tXrX@tc8xjhlgiapgu4fB<57_vzoi{kz{XeERs4ftiJg z;Xlyye?V*g0~tUjg#RBxgZL~!f)~j8%FM~|_3PjN00G4E@$+AXcx^@oM@=Dye_wwx z{0BObnTL&mk(r5sg@pyE`5(~0zhE{aBO?O?*wDWWTQ7WPNZ)u00{Z{~ z01FWg01)ox0QRP90Pd?(00HCH015~L0PX7K0Q>m!01piZ0P^tb0PE@H0PE=F0Q~s# z00;;M0R90300M~RF9S2fKL#d-->+XW2!2|@z$N&X;m_Mw46omRVOYFmC4+&19)qi^ z6T_xWI~ZQQdd=|X&mRUSYeNPmF(fg$?z8H2#LsSF$f zpBetX3}$%FFUBx`&Kw3^T^$B{dpm|@%T_YHe*G34h}I_B49vogz|_FR01!aHSO6OE z59lue9tOtO8Vvv6|72h?7i16=6<~;qi(+75W(C^(2N)C1Ko|W6yFi$ig<=20UqCN1 zF#rS*%U_@~{(b$#z@V?nz+ZoWf&Vwqt^CYD_x@mz6B7d32+_nY!~^#GU!V)PS^qL{ zFapyw&;Wn{V&UZBWZHj^k>U314-CIq*dZbP;~P-pKX9f22O20-0Xcs`fd~qDpvF5N zSQ&n^voiq%5X+0l&tGla`JdzSw?F^?LDM}*(LZP|{0B^_Kp#Rf0u28HWeQ*-`1bN0 jKmalRhs7b803g5sZD^P}d)&H{00000NkvXXu0mjfb?2h$ literal 0 HcmV?d00001 diff --git a/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/DataGraphProviderTest.java b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/DataGraphProviderTest.java new file mode 100644 index 0000000000..e23fac72e4 --- /dev/null +++ b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/DataGraphProviderTest.java @@ -0,0 +1,531 @@ +/* ### + * 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 datagraph.graph; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import datagraph.*; +import datagraph.data.graph.*; +import datagraph.data.graph.panel.model.row.DataRowObject; +import docking.action.DockingActionIf; +import docking.action.ToggleDockingActionIf; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.services.ProgramManager; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.graph.viewer.*; +import ghidra.graph.viewer.event.picking.GPickedState; +import ghidra.graph.viewer.options.VisualGraphOptions; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.util.ProgramLocation; +import ghidra.test.*; + +public class DataGraphProviderTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private PluginTool tool; + private DataGraphPlugin dataGraphPlugin; + private CodeBrowserPlugin codeBrowser; + private ProgramDB program; + private Structure employerStruct; + private Structure addressStruct; + private Structure personStruct; + private ToyProgramBuilder builder; + private DataGraphProvider provider; + private DegController controller; + private DataExplorationGraph graph; + + @Before + public void setUp() throws Exception { + + setErrorGUIEnabled(false); + createStructures(); + + env = new TestEnv(); + tool = env.getTool(); + + initializeTool(); + goToAddress("0x300"); + graph = showDataGraph(); + turnOffAnimation(); + } + + @Test + public void testGraphHasInitialVertex() { + + DegVertex vertex = graph.getRoot(); + assertTitle("Person_00000300 @ 00000300", vertex); + } + + @Test + public void testExpandInsideVertex() { + assertEquals(1, graph.getVertexCount()); + DegVertex vertex = graph.getRoot(); + + //@formatter:off + assertField(vertex, + "Person", + " Name", + " Age", + " Address", + " Employer" + ); + //@formatter:on + + expand(vertex, " Employer"); + + //@formatter:off + assertField(vertex, + "Person", + " Name", + " Age", + " Address", + " Employer", + " Company", + " Address" + ); + //@formatter:on + } + + @Test + public void testOpenChildNode() { + + assertEquals(1, graph.getVertexCount()); + DegVertex vertex = graph.getRoot(); + + openRow(vertex, " Address"); + + assertEquals(2, graph.getVertexCount()); + DegVertex newVertex = getVertex("Address_00000100 @ 00000100"); + assertNotNull(newVertex); + } + + @Test + public void testOutgoingReferencesAction() { + openOutgoingReferences(); + assertEquals(3, graph.getVertexCount()); + } + + @Test + public void testIncommingReferencesAction() { + openIncomingReferences(); + assertEquals(2, graph.getVertexCount()); + } + + private void openIncomingReferences() { + DockingActionIf inRefsAction = getLocalAction(provider, "Incoming References"); + DegContext context = (DegContext) provider.getActionContext(null); + assertNotNull(context.getVertex()); + + performAction(inRefsAction, context, true); + } + + private void openOutgoingReferences() { + DockingActionIf outRefsAction = getLocalAction(provider, "Outgoing References"); + DegContext context = (DegContext) provider.getActionContext(null); + assertNotNull(context.getVertex()); + + performAction(outRefsAction, context, true); + } + + @Test + public void testCloseSelectedVerticesAction() { + DockingActionIf closeAction = getLocalAction(provider, "Delete Vertices"); + DegContext context = (DegContext) provider.getActionContext(null); + assertEquals(1, context.getSelectedVertices().size()); + assertFalse(closeAction.isEnabledForContext(context)); + + openOutgoingReferences(); + Set newVertices = getNonRootVertices(); + selectVertices(newVertices); + + assertEquals(2, context.getSelectedVertices().size()); + assertTrue(closeAction.isEnabledForContext(context)); + + performAction(closeAction); + assertEquals(1, graph.getVertexCount()); + + } + + @Test + public void testOrientGraphAction() { + openOutgoingReferences(); + DegVertex root = graph.getRoot(); + + Set newVertices = getNonRootVertices(); + DegVertex other = newVertices.iterator().next(); + assertNull(root.getSourceVertex()); + assertEquals(root, other.getSourceVertex()); + + selectVertices(Set.of(other)); + DockingActionIf orientAction = getLocalAction(provider, "Set Original Vertex"); + DegContext context = (DegContext) provider.getActionContext(null); + performAction(orientAction, context, true); + assertEquals(other, graph.getRoot()); + + assertEquals(other, root.getSourceVertex()); + assertNull(other.getSourceVertex()); + + } + + @Test + public void testExpandFormatAction() { + assertTrue(controller.isCompactFormat()); + ToggleDockingActionIf expandedFormatAction = + (ToggleDockingActionIf) getLocalAction(provider, "Show Expanded Format"); + + performAction(expandedFormatAction, true); + assertFalse(controller.isCompactFormat()); + + } + + @Test + public void testNavigationOut() { + openOutgoingReferences(); + Collection vertices = graph.getVertices(); + for (DegVertex dgVertex : vertices) { + selectVertices(Set.of(dgVertex)); + Address vertexAddress = dgVertex.getAddress(); + Address listingAddress = codeBrowser.getCurrentAddress(); + assertEquals(vertexAddress, listingAddress); + } + } + + @Test + public void testNavigateIn() { + openOutgoingReferences(); + turnOnNavigationIn(); + + goToListing(0x100); + DegVertex focused = getFocusedVertex(); + assertEquals(0x100, focused.getAddress().getOffset()); + + goToListing(0x200); + focused = getFocusedVertex(); + assertEquals(0x200, focused.getAddress().getOffset()); + + goToListing(0x300); + focused = getFocusedVertex(); + assertEquals(0x300, focused.getAddress().getOffset()); + + } + + @Test + public void testVertexCloseAction() { + openRow(graph.getRoot(), " Address"); + DegVertex newVertex = getVertex("Address_00000100 @ 00000100"); + DockingActionIf closeAction = newVertex.getAction("Close Vertex"); + assertTrue(closeAction.isEnabledForContext(provider.getActionContext(null))); + + assertEquals(2, graph.getVertexCount()); + performAction(closeAction); + assertEquals(1, graph.getVertexCount()); + } + + @Test + public void testCantCloseRootVertex() { + DegVertex root = graph.getRoot(); + DockingActionIf closeAction = root.getAction("Close Vertex"); + assertFalse(closeAction.isEnabledForContext(provider.getActionContext(null))); + } + + @Test + public void testExpandAllCollapseAllAction() { + DegVertex root = graph.getRoot(); + DockingActionIf expandAction = root.getAction("Expand All"); + DockingActionIf collapseAction = root.getAction("Collapse All"); + + assertRowCount(root, 5); + + performAction(expandAction); + + assertRowCount(root, 47); + + performAction(collapseAction); + + assertRowCount(root, 1); + } + + private void assertRowCount(DegVertex vertex, int expectedRowCount) { + List rowObjects = ((DataDegVertex) vertex).getRowObjects(); + assertEquals(expectedRowCount, rowObjects.size()); + } + + private DegVertex getFocusedVertex() { + Collection vertices = graph.getVertices(); + for (DegVertex dgVertex : vertices) { + if (dgVertex.isFocused()) { + return dgVertex; + } + } + return null; + } + + private void goToListing(long address) { + runSwing(() -> codeBrowser.goTo(new ProgramLocation(program, builder.addr(address)))); + } + + private void turnOnNavigationIn() { + ToggleDockingActionIf navigateInAction = + (ToggleDockingActionIf) getLocalAction(provider, + "Navigate on Incoming Location Changes"); + + performAction(navigateInAction, true); + + } + + private Set getNonRootVertices() { + Set set = new HashSet<>(graph.getVertices()); + set.remove(graph.getRoot()); + return set; + } + + private void openRow(DegVertex vertex, String text) { + int row = getRowNumber(vertex, text); + runSwing(() -> ((DataDegVertex) vertex).openPointerReference(row)); + waitForAnimation(); + } + + private DegVertex getVertex(String title) { + Collection vertices = graph.getVertices(); + for (DegVertex v : vertices) { + String vertexTitle = v.getTitle(); + if (title.equals(vertexTitle)) { + return v; + } + } + return null; + } + + private void expand(DegVertex v, String text) { + + int row = getRowNumber(v, text); + DataDegVertex dataVertex = (DataDegVertex) v; + runSwing(() -> dataVertex.expand(row)); + } + + private int getRowNumber(DegVertex v, String text) { + List actualRows = getRowsAsText((DataDegVertex) v); + int row = actualRows.indexOf(text); + assertTrue(row >= 0); + return row; + } + + private void assertField(DegVertex v, String... expectedRows) { + + List actualRows = getRowsAsText((DataDegVertex) v); + assertEquals(expectedRows.length, actualRows.size()); + List expectedList = Arrays.asList(expectedRows); + assertListEqualOrdered(expectedList, actualRows); + } + + private List getRowsAsText(DataDegVertex v) { + + List rows = runSwing(() -> v.getRowObjects()); + + //@formatter:off + List asText = rows.stream().map(r -> { + int indent = r.getIndentLevel(); + String name = indent == 0 ? r.getDataType() : r.getName(); + String indentation = StringUtils.repeat("\t", indent); + return indentation + name; + }) + .collect(Collectors.toList()); + //@formatter:on + + return asText; + } + + private void assertTitle(String expected, DegVertex vertex) { + String actual = runSwing(() -> vertex.getTitle()); + assertEquals(expected, actual); + } + + private DataExplorationGraph showDataGraph() { + DockingActionIf action = getAction(dataGraphPlugin, "Display Data Graph"); + assertNotNull(action); + performAction(action); + provider = waitForComponentProvider(DataGraphProvider.class); + controller = provider.getController(); + return controller.getGraph(); + } + + protected ProgramLocation getLocationForAddressString(String addressString) { + Address address = builder.addr(addressString); + return new ProgramLocation(program, address); + } + + protected void goToAddress(String addressString) { + ProgramLocation location = getLocationForAddressString(addressString); + codeBrowser.goTo(location, true); + + waitForSwing(); + } + + protected void initializeTool() throws Exception { + installPlugins(); + + createProgram(); + ProgramManager pm = tool.getService(ProgramManager.class); + pm.openProgram(program.getDomainFile()); + + showTool(tool); + } + + protected void installPlugins() throws PluginException { + tool.addPlugin(CodeBrowserPlugin.class.getName()); + tool.addPlugin(DataGraphPlugin.class.getName()); + + dataGraphPlugin = env.getPlugin(DataGraphPlugin.class); + codeBrowser = env.getPlugin(CodeBrowserPlugin.class); + } + + protected void createProgram() throws Exception { + + builder = new ToyProgramBuilder("sample", true); + builder.createMemory("data", "0x0100", 0x1000); + createStructures(); + + createAddressData(0x100, "123 Main St", "Springfield", "MD", "12211"); + createAddressData(0x200, "987 1st St", "Columbia", "MD", "22331"); + + createPersonData(0x300, "Jane Doe", 32, 0x100, "IBM", 0x200); + + builder.putAddress("0x400", "0x300"); + builder.applyDataType("0x400", new PointerDataType(personStruct)); + + program = builder.getProgram(); + } + + private void createAddressData(long addr, String street, String city, String state, + String zip) throws Exception { + + builder.setString(addrString(addr), street); + builder.setString(addrString(addr + 20), city); + builder.setString(addrString(addr + 40), state); + builder.setString(addrString(addr + 42), zip); + builder.applyDataType(addrString(addr), addressStruct); + } + + private void createPersonData(long addr, String name, int age, long addressPointer, + String companyName, long companyAddressPointer) throws Exception { + + builder.setString(addrString(addr), name); + builder.setInt(addrString(addr + 20), age); + builder.putAddress(addrString(addr + 24), addrString(addressPointer)); + builder.setString(addrString(addr + 28), companyName); + builder.putAddress(addrString(addr + 48), addrString(companyAddressPointer)); + + builder.applyDataType(addrString(addr), personStruct); + } + + private String addrString(long offset) { + return builder.addr(offset).toString(); + } + + private void createStructures() { + employerStruct = createEmployerStruct(); + addressStruct = createAddressStruct(); + personStruct = createPersonStruct(); + } + + private Structure createPersonStruct() { + Structure person = new StructureDataType("Person", 0); + + person.add(getCharField(20), "Name", ""); + person.add(new IntegerDataType(), "Age", ""); + person.add(new PointerDataType(addressStruct), "Address", ""); + person.add(employerStruct, "Employer", ""); + + return person; + } + + private Structure createEmployerStruct() { + Structure employer = new StructureDataType("Employer", 0); + employer.add(getCharField(20), "Company", ""); + employer.add(new PointerDataType(addressStruct), "Address", ""); + return employer; + } + + private Structure createAddressStruct() { + Structure address = new StructureDataType("Address", 0); + + address.add(getCharField(20), "Street", ""); + address.add(getCharField(20), "City", ""); + address.add(getCharField(2), "State", ""); + address.add(getCharField(5), "Zip", ""); + return address; + } + + private DataType getCharField(int size) { + return new ArrayDataType(new CharDataType(), size); + } + + private void waitForAnimation() { + + VisualGraphViewUpdater updater = getGraphUpdater(); + if (updater == null) { + return; // nothing to wait for; no active graph + } + + waitForSwing(); + int tryCount = 3; + while (tryCount++ < 5 && updater.isBusy()) { + waitForConditionWithoutFailing(() -> !updater.isBusy()); + } + waitForSwing(); + + assertFalse(updater.isBusy()); + } + + private VisualGraphViewUpdater getGraphUpdater() { + GraphViewer viewer = controller.getPrimaryViewer(); + VisualGraphViewUpdater updater = viewer.getViewUpdater(); + assertNotNull(updater); + return updater; + } + + private void selectVertices(Set newVertices) { + GraphViewer viewer = controller.getPrimaryViewer(); + GPickedState pickState = viewer.getGPickedVertexState(); + runSwing(() -> { + pickState.clear(); + for (DegVertex dgVertex : newVertices) { + pickState.pick(dgVertex, true); + } + }); + waitForSwing(); + } + + private void turnOffAnimation() { + runSwing(() -> { + GraphComponent comp = + controller.getView().getGraphComponent(); + VisualGraphOptions graphOptions = comp.getGraphOptions(); + graphOptions.setUseAnimation(false); + }); + } + +} diff --git a/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/EgGraphLayoutTest.java b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/EgGraphLayoutTest.java new file mode 100644 index 0000000000..d81c60147a --- /dev/null +++ b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/EgGraphLayoutTest.java @@ -0,0 +1,304 @@ +/* ### + * 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 datagraph.graph; + +import static org.junit.Assert.*; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.Collection; +import java.util.Comparator; + +import javax.swing.*; + +import org.junit.Before; +import org.junit.Test; + +import datagraph.graph.explore.*; +import docking.test.AbstractDockingTest; +import docking.widgets.label.GDLabel; +import generic.theme.GThemeDefaults.Colors.Palette; +import ghidra.graph.VisualGraph; +import ghidra.graph.graphs.DefaultVisualGraph; +import ghidra.graph.viewer.GraphComponent; +import ghidra.graph.viewer.layout.AbstractVisualGraphLayout; +import ghidra.util.Swing; + +public class EgGraphLayoutTest extends AbstractDockingTest { + private static int VERTEX_SIZE = 50; + private static int VERTEX_GAP = 100; + private TestExplorationGraph g; + + private TestVertex root = new TestVertex(null, "R", VERTEX_SIZE, VERTEX_SIZE); + private GraphComponent graphComponent; + + @Before + public void setUp() { + g = new TestExplorationGraph(root); + } + + @Test + public void testGraphOneOutgoingChild() { + TestVertex A = v(root, "A"); + edge(root, A); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedX = VERTEX_GAP + root.width / 2 + A.width / 2; + assertEquals(p(expectedX, 0), A.getLocation()); + } + + @Test + public void testGraphTwoOutgoingChildren() { + TestVertex A = v(root, "A"); + TestVertex B = v(root, "B"); + edge(root, A); + edge(root, B); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedX = VERTEX_GAP + root.width / 2 + A.width / 2; + int totalHeight = VERTEX_GAP + A.height + B.height; + int expectedYa = -totalHeight / 2 + A.height / 2; + int expectedYb = totalHeight / 2 - B.height / 2; + assertEquals(p(expectedX, expectedYa), A.getLocation()); + assertEquals(p(expectedX, expectedYb), B.getLocation()); + } + + @Test + public void testGraphOneIncomingChild() { + TestVertex A = v(root, "A", VERTEX_SIZE, VERTEX_SIZE); + edge(A, root); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedX = -VERTEX_GAP - root.width / 2 - A.width / 2; + assertEquals(p(expectedX, 0), A.getLocation()); + } + + @Test + public void testGraphTwoIncomingChildren() { + TestVertex A = v(root, "A"); + TestVertex B = v(root, "B"); + edge(A, root); + edge(B, root); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedX = -VERTEX_GAP - root.width / 2 - A.width / 2; + int expectedYa = -VERTEX_GAP / 2 - A.height / 2; + int expectedYb = VERTEX_GAP / 2 + B.height / 2; + assertEquals(p(expectedX, expectedYa), A.getLocation()); + assertEquals(p(expectedX, expectedYb), B.getLocation()); + } + + @Test + public void testGraphTwoOutgoingChildrenDifferentSize() { + TestVertex A = v(root, "A"); + TestVertex B = bigV(root, "B"); + edge(root, A); + edge(root, B); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedXa = VERTEX_GAP + root.width / 2 + A.width / 2; + int expectedXb = VERTEX_GAP + root.width / 2 + B.width / 2; + int totalHeight = VERTEX_GAP + A.height + B.height; + int expectedYa = -totalHeight / 2 + A.height / 2; + int expectedYb = totalHeight / 2 - B.height / 2; + assertEquals(p(expectedXa, expectedYa), A.getLocation()); + assertEquals(p(expectedXb, expectedYb), B.getLocation()); + } + + @Test + public void testGraphTwoIncomingChildrenDifferentSize() { + TestVertex A = v(root, "A"); + TestVertex B = bigV(root, "B"); + edge(A, root); + edge(B, root); + + showGraph(); + + assertEquals(p(0, 0), root.getLocation()); + int expectedXa = -VERTEX_GAP - root.width / 2 - A.width / 2; + int expectedXb = -VERTEX_GAP - root.width / 2 - B.width / 2; + int totalHeight = VERTEX_GAP + A.height + B.height; + int expectedYa = -totalHeight / 2 + A.height / 2; + int expectedYb = totalHeight / 2 - B.height / 2; + assertEquals(p(expectedXa, expectedYa), A.getLocation()); + assertEquals(p(expectedXb, expectedYb), B.getLocation()); + } + + private Point p(int x, int y) { + return new Point(x, y); + } + + protected void showGraph() { + + Swing.runNow(() -> { + JFrame frame = new JFrame("Graph Viewer Test"); + + TestEgGraphLayout layout = new TestEgGraphLayout(g, root); + g.setLayout(layout); + graphComponent = new GraphComponent<>(g); + graphComponent.setSatelliteVisible(false); + + frame.setSize(new Dimension(800, 800)); + frame.setLocation(2400, 100); + frame.getContentPane().add(graphComponent.getComponent()); + frame.setVisible(true); + frame.validate(); + }); + } + + protected TestVertex bigV(TestVertex source, String name) { + TestVertex v = new TestVertex(source, name, VERTEX_SIZE * 2, VERTEX_SIZE * 2); + g.addVertex(v); + return v; + } + + protected TestVertex v(TestVertex source, String name) { + TestVertex v = new TestVertex(source, name, VERTEX_SIZE, VERTEX_SIZE); + g.addVertex(v); + return v; + } + + private TestVertex v(TestVertex source, String name, int width, int height) { + TestVertex v = new TestVertex(source, name, width, height); + g.addVertex(v); + return v; + } + + protected TestEdge edge(TestVertex v1, TestVertex v2) { + TestEdge testEdge = new TestEdge(v1, v2); + g.addEdge(testEdge); + return testEdge; + } + + private class TestVertex extends EgVertex { + + private JLabel label; + private String name; + private int width; + private int height; + + TestVertex(TestVertex source, String name, int width, int height) { + super(source); + this.name = name; + this.width = width; + this.height = height; + } + + @Override + public String toString() { + return name; + } + + @Override + public JComponent getComponent() { + if (label == null) { + label = new GDLabel(); + label.setText(name); + label.setPreferredSize(new Dimension(width, height)); + label.setBackground(Palette.GOLD); + label.setOpaque(true); + label.setBorder(BorderFactory.createRaisedBevelBorder()); + label.setHorizontalAlignment(SwingConstants.CENTER); + } + return label; + } + + @Override + protected Point2D getStartingEdgePoint(EgVertex end) { + return new Point(0, 0); + } + + @Override + protected Point2D getEndingEdgePoint(EgVertex start) { + return new Point(0, 0); + } + + } + + private class TestEdge extends EgEdge { + + public TestEdge(TestVertex start, TestVertex end) { + super(start, end); + } + + @SuppressWarnings("unchecked") + // Suppressing warning on the return type; we know our class is the right type + @Override + public TestEdge cloneEdge(TestVertex start, TestVertex end) { + return new TestEdge(start, end); + } + + } + + private class TestExplorationGraph extends AbstractExplorationGraph { + + TestExplorationGraph(TestVertex root) { + super(root); + } + + @Override + public DefaultVisualGraph copy() { + Collection v = getVertices(); + Collection e = getEdges(); + TestExplorationGraph newGraph = new TestExplorationGraph(getRoot()); + v.forEach(newGraph::addVertex); + e.forEach(newGraph::addEdge); + return newGraph; + } + + } + + private static class TestEgGraphLayout + extends EgGraphLayout { + + protected TestEgGraphLayout(TestExplorationGraph graph, TestVertex root) { + super(graph, "Test", VERTEX_GAP, VERTEX_GAP); + } + + @Override + public AbstractVisualGraphLayout createClonedLayout( + VisualGraph newGraph) { + throw new UnsupportedOperationException(); + } + + @Override + protected EgEdgeTransformer createEdgeTransformer() { + return new EgEdgeTransformer(); + } + + @Override + protected Comparator getIncommingVertexComparator() { + return (v1, v2) -> v1.name.compareTo(v2.name); + } + + @Override + protected Comparator getOutgoingVertexComparator() { + return (v1, v2) -> v1.name.compareTo(v2.name); + } + + } +} diff --git a/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/GraphLocationMapTest.java b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/GraphLocationMapTest.java new file mode 100644 index 0000000000..6330545f0e --- /dev/null +++ b/Ghidra/Features/DataGraph/src/test/java/datagraph/graph/GraphLocationMapTest.java @@ -0,0 +1,91 @@ +/* ### + * 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 datagraph.graph; + +import static org.junit.Assert.*; + +import java.awt.Point; + +import org.junit.Test; + +import datagraph.graph.explore.GraphLocationMap; + +public class GraphLocationMapTest { + + private GraphLocationMap map1; + private GraphLocationMap map2; + + @Test + public void testInitialState() { + TestVertex v = new TestVertex("A"); + assertEquals("A", v.getName()); + map1 = new GraphLocationMap<>(v, 10, 20); + + assertEquals(10, map1.getWidth()); + assertEquals(20, map1.getHeight()); + assertEquals(p(0, 0), map1.get(v)); + } + + @Test + public void testMergeRight() { + TestVertex a = new TestVertex("A"); + TestVertex b = new TestVertex("B"); + + map1 = new GraphLocationMap<>(a, 10, 20); + map2 = new GraphLocationMap<>(b, 100, 50); + + // merge maps left to right, shifting map2 by 1000 + map1.merge(map2, 1000, 0); + + assertEquals(1055, map1.getWidth()); // width of both maps + the gap + assertEquals(50, map1.getHeight()); // height of the tallest map + assertEquals(p(0, 0), map1.get(a)); // point in first map doesn't move + assertEquals(p(1000, 0), map1.get(b));// point in second map moved by shift + } + + @Test + public void testMergeBottom() { + TestVertex a = new TestVertex("A"); + TestVertex b = new TestVertex("B"); + + map1 = new GraphLocationMap<>(a, 10, 20); + map2 = new GraphLocationMap<>(b, 100, 50); + + // merge maps left to right, shifting map2 by 1000 + map1.merge(map2, 0, 1000); + + assertEquals(100, map1.getWidth()); // width of both maps + the gap + assertEquals(1035, map1.getHeight()); // height of the tallest map + assertEquals(p(0, 0), map1.get(a)); // point in first map doesn't move + assertEquals(p(0, 1000), map1.get(b));// point in second map moved by shift + } + + private Point p(int x, int y) { + return new Point(x, y); + } + + private class TestVertex { + private String name; + + TestVertex(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} 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 50b978faac..b6bd757d1f 100644 --- a/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/FunctionGraph/src/main/help/help/TOC_Source.xml @@ -54,7 +54,7 @@ - + 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 c4d919975b..feb5225643 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 @@ -29,7 +29,7 @@ "help/topics/CodeBrowserPlugin/CodeBrowser.htm">Listing.

The display consists of the Primary View and the Satellite View. There is also a group of Satellite View. There is also a group of actions that apply to the entire graph.

@@ -94,60 +94,6 @@ Automatic Graph Relayout

-

Satellite View

- -
-

The Satellite View provides an overview of the graph. From this view you may also perform - basic adjustment of the overall graph location. In addition to the complete graph, the - satellite view contains a lens (the white rectangle) that indicates how much of the - current graph fits into the primary view.

- -

When you single left mouse click in the satellite view the graph is centered around the - corresponding point in the primary view. Alternatively, you may drag the lens of the - satellite view to the desired location by performing a mouse drag operation on the lens.

- -

You may hide the satellite view by right-clicking anywhere in the Primary View and - deselecting the Display Satellite View toggle button from the popup menu.

- -
-

If the Primary View is painting - sluggishly, then hiding the Satellite View cause the Primary View to be more - responsive.

-
- -

Detached Satellite

- -
-

The Satellite View is attached, or docked, to the Primary View by default. - However, you can detach, or undock, the Satellite View, which will put the view into a - Component Provider, which itself can be moved, resized and docked anywhere in the Tool you - wish.

- -

To undock the Satellite View, right-click in the graph and deselect the Dock - Satellite View menu item.

- -

To re-dock the Satellite View, right-click in the graph and select the Dock Satellite - View menu item.

-
- -
-

To reshow the Satellite View if it is - hidden, whether docked or undocked, you can press the button. This button is in the lower-right - hand corner of the graph and is only visible if the Satellite View is hidden or - undocked.

-
- - -

Docked Satellite Location

-
-

When the Satellite View is attached, or docked, to the Primary View, you - can choose which corner to show the satellite view. To change the - corner, right-click in the graph, select Docked Satellite Position and then - select the appropriate sub-menu for the desired corner.

-

-
-

Vertices (Blocks)

@@ -896,54 +842,16 @@
-

Panning

- -
-

There are various ways to move the graph. To move the graph in any direction you can drag - from the whitespace of the graph.

- -

By default, to move the graph vertically you can use the mouse wheel. In previous releases - the scroll wheel was used to zoom. Now there is an option to restore that behavior, the - Scroll Wheel Pans option. When this option is on, you can zoom by holding the - Control key (Command key on the Mac) while using the scroll - wheel. Alternatively, you can move the graph left to right using the mouse while - holding Control-Alt.

- -

The satellite viewer may also be used to move the primary graphs view by dragging and - clicking inside of the satellite viewer.

-
-

Zooming

At full zoom, or block level zoom, each block is rendered at its natural size, which is the same scale as Ghidra's primary Listing. From that point, which is a 1:1 - zoom level, you can zoom out in order to fit more of the graph into the display.

- -
-

To change the zoom you may use the mouse scroll wheel while holding the - Control key (Command key on the Mac). This works whether - the mouse is over the primary viewer or the satellite viewer. Also, you may use the context - popup menu from the primary viewer in order to quickly zoom to the block level (1:1) or to - the window level (zoomed out far enough to fit the entire graph in the window). These - actions are Zoom to Vertex and Zoom to Window, respectively.

- -

Note To have the scroll wheel zoom - without holding the Control key, you can disable the Scroll Wheel - Pans option.

- -

Note To zoom the graph incrementally - using the keyboard you can use the Zoom In and Zoom Out actions. These - actions have default keybindings of Control-Minus and - Control-Equals.

-
- -

The satellite viewer is always zoomed out far enough to fit - the entire graph into its window.

-
- - + zoom level, you can zoom out in order to fit more of the graph into the display. See + Visual Graph Zooming for + more information on graph zooming.

+

Vertex Quick Zoom

If you double-click a block header, then the Zoom Level of the @@ -954,48 +862,6 @@

Interaction Threshold

- -
-

While zooming out (away from the blocks) you will eventually reach a point where you can - no longer interact with the listing inside of the block. The blocks provide a subtle visual - indication when they are zoomed past this level, in the form of a drop-shadow. The image - below shows this drop-shadow. The block on the left is not past the interaction threshold, - but the block on the right is, and thus has a drop-shadow. This example is for illustrative - purposes only and during normal usage all blocks will share the save zoom level. So, if one - block is zoomed past the interaction threshold, all other blocks will be as well.

- - - - - - - -
- -

- Interaction with blocks that are past the interaction threshold is simplified; for - example, when scaled past the interaction threshold, dragging in the listing area of a block - will drag the block, instead of making a selection in the listing, as would happen when not - scaled past the interaction threshold. -

-
-
-
- - -

Painting Threshold

-
-

While zooming out (away from the blocks) you will eventually reach a point where contents - each block will not be painted. Instead, each block will be painted by a rectangle that is - painted with the current background color of the block.

- -
-

Note Zooming past the painting - threshold will improve the rendering speed of the Primary View.

-
-
-

Saving View Information

@@ -1015,6 +881,19 @@ +

Standard Graph Features and Actions

+
+

The function graph is a type of Ghidra Visual Graph and has some standard concepts, features + and actions. +

+ +
+

Provided by: Function Graph Plugin

diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java index c92bff3606..bfb708657f 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java @@ -173,7 +173,7 @@ public class FunctionGraph extends GroupingVisualGraph { @Override public void vertexLocationChanged(FGVertex v, Point point, ChangeType changeType) { - if (changeType == ChangeType.USER) { + if (!changeType.isTransitional()) { settings.putVertexLocation(v, point); } } diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/EmptyLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/EmptyLayout.java index f5a125d745..344721b3a3 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/EmptyLayout.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/EmptyLayout.java @@ -4,9 +4,9 @@ * 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. @@ -20,6 +20,7 @@ import java.awt.geom.Point2D; import com.google.common.base.Function; +import edu.uci.ics.jung.visualization.RenderContext; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import ghidra.app.plugin.core.functiongraph.graph.FGEdge; import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; @@ -56,7 +57,8 @@ public class EmptyLayout extends AbstractVisualGraphLayout imp } @Override - public Function getEdgeShapeTransformer() { + public Function getEdgeShapeTransformer( + RenderContext context) { return new ArticulatedEdgeTransformer<>(); } diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java index 9cc92c4d0a..a2ec1b2d39 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java @@ -97,7 +97,7 @@ public abstract class AbstractFlowChartLayout positions = LayoutPositions.createNewPositions(vertexMap, edgeMap); // DEGUG triggers grid lines to be printed; useful for debugging - // VisualGraphRenderer.setGridPainter(new GridPainter(layoutMap.getGridCoordinates())); +// VisualGraphRenderer.setGridPainter(new GridPainter(layoutMap.getGridCoordinates())); layoutMap.dispose(); return positions; diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java index 9d180a569f..0bfe146152 100644 --- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java @@ -36,9 +36,9 @@ public class FlowChartLayoutTest extends AbstractFlowChartLayoutTest { @Test public void testBasicRootWithTwoChildren() throws CancelledException { + edge(A, B); edge(A, C); - applyLayout(); showGraph(); assertVertices(""" @@ -67,7 +67,7 @@ public class FlowChartLayoutTest extends AbstractFlowChartLayoutTest { edge(A, D); applyLayout(); -// showGraph(); + showGraph(); assertVertices(""" ...... diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java b/Ghidra/Features/FunctionGraph/src/test/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java similarity index 99% rename from Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java rename to Ghidra/Features/FunctionGraph/src/test/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java index 08c4e810d4..551d1ff56c 100644 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java +++ b/Ghidra/Features/FunctionGraph/src/test/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java @@ -21,8 +21,6 @@ import java.util.*; import org.junit.Test; -import ghidra.graph.graphs.TestEdge; -import ghidra.graph.graphs.TestVertex; import ghidra.graph.viewer.layout.GridPoint; public class EdgeSegmentTest { diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java index 59c0bef93a..cf65c6e297 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java @@ -324,7 +324,7 @@ public class FcgVertexShapeProvider extends CircleWithLabelVertexShapeProvider { } @Override - protected void setTogglesVisible(boolean visible) { + public void setTogglesVisible(boolean visible) { boolean isIn = isInDirection(); boolean turnOn = isIn && hasIncomingReferences && visible; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockableComponent.java b/Ghidra/Framework/Docking/src/main/java/docking/DockableComponent.java index cd6cd48777..bc79c0a14c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockableComponent.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockableComponent.java @@ -147,6 +147,7 @@ public class DockableComponent extends JPanel implements ContainerListener { } private void showContextMenu(MouseEvent e) { + if (e.isConsumed()) { return; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java b/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java index f013cef5c8..c2a78a7782 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java @@ -23,6 +23,7 @@ import javax.swing.*; import docking.action.*; import docking.menu.*; +import generic.theme.CloseIcon; import generic.theme.GColor; import ghidra.util.exception.AssertException; import ghidra.util.task.SwingUpdateManager; @@ -32,7 +33,7 @@ import ghidra.util.task.SwingUpdateManager; */ class DockableToolBarManager { private static final Color BUTTON_COLOR = new GColor("color.fg.button"); - private static final Icon CLOSE_ICON = new CloseIcon(false, BUTTON_COLOR); + private static final Icon CLOSE_ICON = new CloseIcon(false); private Icon MENU_ICON = new DropDownMenuIcon(BUTTON_COLOR); private GenericHeader dockableHeader; private ToolBarManager toolBarManager; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/GenericHeader.java b/Ghidra/Framework/Docking/src/main/java/docking/GenericHeader.java index 87ecd2964f..2d01c78a5b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/GenericHeader.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/GenericHeader.java @@ -4,9 +4,9 @@ * 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. @@ -277,6 +277,10 @@ public class GenericHeader extends JPanel { titlePanel.setTitle(title); } + public String getTitle() { + return titlePanel.getTitle(); + } + public void setIcon(Icon icon) { titlePanel.setIcon(icon); } @@ -396,6 +400,10 @@ public class GenericHeader extends JPanel { titleLabel.setToolTipText(s); } + String getTitle() { + return titleLabel.getText(); + } + void setIcon(Icon icon) { icon = DockingUtils.scaleIconAsNeeded(icon); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tabbedpane/DockingTabRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tabbedpane/DockingTabRenderer.java index b3b12d50a0..4e1a92fef0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tabbedpane/DockingTabRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tabbedpane/DockingTabRenderer.java @@ -20,10 +20,9 @@ import java.awt.event.*; import javax.swing.*; -import docking.CloseIcon; import docking.widgets.EmptyBorderButton; import docking.widgets.label.GDLabel; -import generic.theme.GColor; +import generic.theme.CloseIcon; /** * A widget that can be used to render an icon, title and close button for JTabbedPane. You would @@ -32,7 +31,7 @@ import generic.theme.GColor; public class DockingTabRenderer extends JPanel { private static final int MAX_TITLE_LENGTH = 25; - private Icon CLOSE_ICON = new CloseIcon(true, new GColor("color.fg.button")); + private Icon CLOSE_ICON = new CloseIcon(true); private JLabel titleLabel; private JLabel iconLabel; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/AbstractGTrableRowModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/AbstractGTrableRowModel.java new file mode 100644 index 0000000000..f77511185e --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/AbstractGTrableRowModel.java @@ -0,0 +1,45 @@ +/* ### + * 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 docking.widgets.trable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for GTrable models. Adds support for listeners. + * + * @param the row data object type + */ +public abstract class AbstractGTrableRowModel implements GTrableRowModel { + private List listeners = new ArrayList<>(); + + @Override + public void addListener(GTrableModeRowlListener l) { + listeners.add(l); + } + + @Override + public void removeListener(GTrableModeRowlListener l) { + listeners.remove(l); + } + + protected void fireModelChanged() { + for (GTrableModeRowlListener listener : listeners) { + listener.trableChanged(); + } + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableCellRenderer.java new file mode 100644 index 0000000000..ace46aa1f1 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableCellRenderer.java @@ -0,0 +1,51 @@ +/* ### + * 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 docking.widgets.trable; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.table.DefaultTableCellRenderer; + +/** + * Base class for GTrable cell renderers. + * + * @param the data model row object type + */ +public class DefaultGTrableCellRenderer extends DefaultTableCellRenderer + implements GTrableCellRenderer { + + @Override + public Component getCellRenderer(GTrable trable, T value, boolean isSelected, + boolean hasFocus, int row, int column) { + + if (trable == null) { + return this; + } + + Color fg = isSelected ? trable.getSelectionForeground() : trable.getForeground(); + Color bg = isSelected ? trable.getSelectionBackground() : trable.getBackground(); + super.setForeground(fg); + super.setBackground(bg); + + setFont(trable.getFont()); + setValue(value); + + return this; + + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableRowModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableRowModel.java new file mode 100644 index 0000000000..a836052433 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/DefaultGTrableRowModel.java @@ -0,0 +1,98 @@ +/* ### + * 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 docking.widgets.trable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation for a simple {@link GTrable} row data model. + * + * @param the row object type + */ +public class DefaultGTrableRowModel> extends AbstractGTrableRowModel { + protected List rows; + + public DefaultGTrableRowModel(List roots) { + this.rows = new ArrayList<>(roots); + } + + @Override + public int getRowCount() { + return rows.size(); + } + + @Override + public T getRow(int index) { + return rows.get(index); + } + + @Override + public int getIndentLevel(int rowIndex) { + return rows.get(rowIndex).getIndentLevel(); + } + + @Override + public boolean isExpanded(int rowIndex) { + return rows.get(rowIndex).isExpanded(); + } + + @Override + public boolean isExpandable(int rowIndex) { + return rows.get(rowIndex).isExpandable(); + } + + @Override + public int collapseRow(int lineIndex) { + T row = rows.get(lineIndex); + int indentLevel = row.getIndentLevel(); + int removedCount = removeIndentedRows(lineIndex + 1, indentLevel + 1); + row.setExpanded(false); + fireModelChanged(); + return removedCount; + + } + + protected int removeIndentedRows(int startIndex, int indentLevel) { + int endIndex = findNextIndexAtLowerIndentLevel(startIndex, indentLevel); + rows.subList(startIndex, endIndex).clear(); + return endIndex - startIndex; + } + + protected int findNextIndexAtLowerIndentLevel(int startIndex, int indentLevel) { + for (int i = startIndex; i < rows.size(); i++) { + T line = rows.get(i); + if (line.getIndentLevel() < indentLevel) { + return i; + } + } + return rows.size(); + } + + @Override + public int expandRow(int lineIndex) { + T row = rows.get(lineIndex); + if (!row.isExpandable() || row.isExpanded()) { + return 0; + } + List children = row.getChildRows(); + rows.addAll(lineIndex + 1, children); + row.setExpanded(true); + fireModelChanged(); + return children.size(); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrable.java new file mode 100644 index 0000000000..e6a71501fc --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrable.java @@ -0,0 +1,646 @@ +/* ### + * 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 docking.widgets.trable; + +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.*; + +import docking.DockingUtils; +import ghidra.util.datastruct.Range; + +/** + * Component that combines the display of a tree and a table. Data is presented in columns like a + * table, but rows can have child rows like a tree which are displayed indented in the first + * column. + *

+ * A GTrable uses two different models: a row model and a column model. The row model contains + * row objects that contains the data to be displayed on a given row. The column model specifies + * how to display the data in the row object as a series of column values. + *

+ * The row model also provides information about the parent child relationship of rows. If the + * model reports that a row can be expanded, an expand control is show on that row. If the row + * is then expanded, the model will then report additional rows immediately below the parent row, + * pushing any existing rows further down (i.e. all rows below the row being opened have their row + * indexes increased by the number of rows added.) + * + * @param The row object type + */ +public class GTrable extends JComponent + implements Scrollable, GTrableModeRowlListener { + private static final int ICON_WIDTH = 16; + private static final int INDENT_WIDTH = 12; + private static final int DEFAULT_MAX_VISIBLE_ROWS = 10; + private static final int DEFAULT_MIN_VISIBLE_ROWS = 10; + private static OpenCloseIcon OPEN_ICON = new OpenCloseIcon(true, ICON_WIDTH, ICON_WIDTH); + private static OpenCloseIcon CLOSED_ICON = new OpenCloseIcon(false, ICON_WIDTH, ICON_WIDTH); + + private Color selectionForground = UIManager.getColor("List.selectionForeground"); + private Color selectionBackground = UIManager.getColor("List.selectionBackground"); + private int minVisibleRows = DEFAULT_MIN_VISIBLE_ROWS; + private int maxVisibleRows = DEFAULT_MAX_VISIBLE_ROWS; + + private int rowHeight = 20; + private GTrableRowModel rowModel; + private GTrableColumnModel columnModel; + private CellRendererPane renderPane; + private int selectedRow = -1; + private List cellClickedListeners = new ArrayList<>(); + private List> selectedRowConsumers = new ArrayList<>(); + + /** + * Constructor + * @param rowModel the model that provides the row data. + * @param columnModel the model the provides the column information for displaying the data + * stored in the row data. + */ + public GTrable(GTrableRowModel rowModel, GTrableColumnModel columnModel) { + this.rowModel = rowModel; + this.columnModel = columnModel; + this.rowModel.addListener(this); + renderPane = new CellRendererPane(); + add(renderPane); + GTrableMouseListener l = new GTrableMouseListener(); + addMouseListener(l); + addMouseMotionListener(l); + addKeyListener(new GTrableKeyListener()); + setFocusable(true); + } + + /** + * Sets a new row model. + * @param newRowModel the new row model to use + */ + public void setRowModel(GTrableRowModel newRowModel) { + rowModel.removeListener(this); + rowModel = newRowModel; + newRowModel.addListener(this); + } + + /** + * Sets a new column model. + * @param columnModel the new column model to use + */ + public void setColumnModel(GTrableColumnModel columnModel) { + this.columnModel = columnModel; + } + + /** + * Sets the preferred number of visible rows to be displayed in the scrollable area. + * @param minVisibleRows the minimum number of visible rows. + * @param maxVisibleRows the maximum number of visible rows. + */ + public void setPreferredVisibleRowCount(int minVisibleRows, int maxVisibleRows) { + this.minVisibleRows = minVisibleRows; + this.maxVisibleRows = maxVisibleRows; + } + + /** + * Adds a listener to be notified if the user clicks on a cell in the GTrable. + * @param listener the listener to be notified + */ + public void addCellClickedListener(GTrableCellClickedListener listener) { + cellClickedListeners.add(listener); + } + + /** + * Removes a cell clicked listener. + * @param listener the listener to be removed + */ + public void removeCellClickedListener(GTrableCellClickedListener listener) { + cellClickedListeners.remove(listener); + } + + /** + * Adds a consumer to be notified when the selected row changes. + * @param consumer the consumer to be notified when the selected row changes + */ + public void addSelectedRowConsumer(Consumer consumer) { + selectedRowConsumers.add(consumer); + } + + /** + * Removes the consumer to be notified when the selected row changes. + * @param consumer the consumer to be removed + */ + public void removeSelectedRowConsumer(Consumer consumer) { + selectedRowConsumers.remove(consumer); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(columnModel.getPreferredWidth(), rowModel.getRowCount() * rowHeight); + } + + @Override + public void paint(Graphics g) { + Rectangle clipBounds = g.getClipBounds(); + int startIndex = getStartIndex(clipBounds); + int endIndex = getEndIndex(clipBounds); + for (int index = startIndex; index <= endIndex; index++) { + drawRow(g, index); + } + } + + /** + * {@return the range of visible row indices.} + */ + public Range getVisibleRows() { + Container parent = getParent(); + Rectangle rect; + if (parent instanceof JViewport viewport) { + rect = viewport.getViewRect(); + } + else { + rect = getVisibleRect(); + } + return new Range(getStartIndex(rect), getEndIndex(rect)); + } + + /** + * {@return the currently selected row or -1 if not row is selected.} + */ + public int getSelectedRow() { + return selectedRow; + } + + /** + * Sets the selected row to the given row index + * @param rowIndex the row index to select + */ + public void setSelectedRow(int rowIndex) { + if (rowIndex >= 0 && rowIndex < rowModel.getRowCount()) { + this.selectedRow = rowIndex; + repaint(); + notifySelectedRowConsumers(); + } + } + + /** + * Deselects any selected row + */ + public void clearSelectedRow() { + this.selectedRow = -1; + repaint(); + } + + /** + * {@return the selection foreground color} + */ + public Color getSelectionForeground() { + return selectionForground; + } + + /** + * {@return the selection background color} + */ + public Color getSelectionBackground() { + return selectionBackground; + } + + /** + * {@return the height of a row in the trable.} + */ + public int getRowHeight() { + return rowHeight; + } + + /** + * {@return the amount the view is scrolled such that the first line is not fully visible.} + */ + public int getRowOffcut() { + Rectangle visibleRect = getVisibleRect(); + int y = visibleRect.y; + return y % rowHeight; + + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + int size = Math.min(rowModel.getRowCount(), maxVisibleRows); + size = Math.max(size, minVisibleRows); + return new Dimension(columnModel.getPreferredWidth(), size * rowHeight); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, + int direction) { + return 5; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, + int direction) { + return 50; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + + /** + * Expands the row at the given index. + * @param rowIndex the index of the row to expand + */ + public void expandRow(int rowIndex) { + int numRowsAdded = rowModel.expandRow(rowIndex); + if (selectedRow > rowIndex) { + setSelectedRow(selectedRow + numRowsAdded); + } + } + + /** + * Collapse the row (remove any of its descendants) at the given row index. + * @param rowIndex the index of the row to collapse + */ + public void collapseRow(int rowIndex) { + int numRowsDeleted = rowModel.collapseRow(rowIndex); + if (selectedRow > rowIndex) { + int newSelectedRow = selectedRow - numRowsDeleted; + if (newSelectedRow < rowIndex) { + newSelectedRow = rowIndex; + } + setSelectedRow(newSelectedRow); + } + } + + /** + * Fully expands the given row and all its descendants. + * @param rowIndex the index of the row to fully expand + */ + public void expandRowRecursively(int rowIndex) { + int startIndentLevel = rowModel.getIndentLevel(rowIndex); + int numRowsAdded = rowModel.expandRow(rowIndex); + if (selectedRow > rowIndex) { + setSelectedRow(selectedRow + numRowsAdded); + } + int nextRow = rowIndex + 1; + while (nextRow < rowModel.getRowCount() && + rowModel.getIndentLevel(nextRow) > startIndentLevel) { + numRowsAdded = rowModel.expandRow(nextRow); + if (selectedRow > nextRow) { + setSelectedRow(selectedRow + numRowsAdded); + } + nextRow++; + } + } + + /** + * Expands all rows fully. + */ + public void expandAll() { + int rowIndex = 0; + for (rowIndex = 0; rowIndex < rowModel.getRowCount(); rowIndex++) { + int indentLevel = rowModel.getIndentLevel(rowIndex); + if (indentLevel == 0) { + expandRowRecursively(rowIndex); + } + } + } + + /** + * Collapses all rows. + */ + public void collapseAll() { + int rowIndex = 0; + for (rowIndex = 0; rowIndex < rowModel.getRowCount(); rowIndex++) { + int indentLevel = rowModel.getIndentLevel(rowIndex); + if (indentLevel == 0) { + collapseRow(rowIndex); + } + } + + } + + /** + * Scrolls the view to make the currently selected row visible. + */ + public void scrollToSelectedRow() { + if (selectedRow < 0) { + return; + } + Container parent = getParent(); + if (!(parent instanceof JViewport viewport)) { + return; + } + Rectangle viewRect = viewport.getViewRect(); + int yStart = selectedRow * rowHeight; + int yEnd = yStart + rowHeight; + if (yStart < viewRect.y) { + viewport.setViewPosition(new Point(0, yStart)); + } + else if (yEnd > viewRect.y + viewRect.height) { + viewport.setViewPosition(new Point(0, yEnd - viewRect.height)); + } + } + + @Override + public void trableChanged() { + setSize(getWidth(), rowModel.getRowCount() * rowHeight); + revalidate(); + repaint(); + } + + @Override + public void setBounds(int x, int y, int width, int height) { + super.setBounds(x, y, width, height); + columnModel.setWidth(width); + } + + private void notifySelectedRowConsumers() { + for (Consumer consumer : selectedRowConsumers) { + consumer.accept(selectedRow); + } + } + + private void notifyCellClicked(int row, int column, MouseEvent e) { + for (GTrableCellClickedListener listener : cellClickedListeners) { + listener.cellClicked(row, column, e); + } + } + + private void drawRow(Graphics g, int rowIndex) { + T row = rowModel.getRow(rowIndex); + int width = getWidth(); + boolean isSelected = rowIndex == selectedRow; + + int y = rowIndex * rowHeight; + + Color fg = isSelected ? selectionForground : getForeground(); + Color bg = isSelected ? selectionBackground : getBackground(); + g.setColor(bg); + g.fillRect(0, y, width, rowHeight); + GTrableColumn firstColumn = columnModel.getColumn(0); + int colWidth = firstColumn.getWidth(); + + int marginWidth = paintLeftMargin(g, rowIndex, y, colWidth, fg); + int x = marginWidth; + paintColumn(g, x, y, colWidth - marginWidth, firstColumn, row, isSelected); + + x = colWidth; + for (int i = 1; i < columnModel.getColumnCount(); i++) { + GTrableColumn column = columnModel.getColumn(i); + colWidth = column.getWidth(); + paintColumn(g, x, y, colWidth, column, row, isSelected); + x += colWidth; + } + } + + private void paintColumn(Graphics g, int x, int y, int width, GTrableColumn column, + T row, boolean isSelected) { + + GTrableCellRenderer renderer = column.getRenderer(); + C columnValue = column.getValue(row); + Component component = renderer.getCellRenderer(this, columnValue, isSelected, false, 0, 0); + + renderPane.paintComponent(g, component, this, x, y, width, rowHeight); + } + + private int paintLeftMargin(Graphics g, int rowIndex, int y, int width, Color fg) { + int x = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH; + drawOpenCloseControl(g, rowIndex, x, y, fg); + return x + ICON_WIDTH; + } + + private void drawOpenCloseControl(Graphics g, int rowIndex, int x, int y, Color fg) { + if (!rowModel.isExpandable(rowIndex)) { + return; + } + OpenCloseIcon icon = rowModel.isExpanded(rowIndex) ? OPEN_ICON : CLOSED_ICON; + icon.setColor(fg); + icon.paintIcon(this, g, x, y + rowHeight / 2 - icon.getIconHeight() / 2); + } + + private int getStartIndex(Rectangle clipBounds) { + if (clipBounds.height == 0) { + return 0; + } + int index = clipBounds.y / rowHeight; + return Math.min(index, rowModel.getRowCount() - 1); + } + + private int getEndIndex(Rectangle clipBounds) { + if (clipBounds.height == 0) { + return 0; + } + int y = clipBounds.y + clipBounds.height - 1; + return Math.min(y / rowHeight, rowModel.getRowCount() - 1); + } + + private void toggleOpen(int rowIndex) { + if (rowIndex < 0) { + return; + } + if (!rowModel.isExpandable(rowIndex)) { + return; + } + if (rowModel.isExpanded(rowIndex)) { + collapseRow(rowIndex); + } + else { + expandRow(rowIndex); + } + } + + private class GTrableMouseListener extends MouseAdapter { + private static final int TRIGGER_MARGIN = 10; + private int startDragX = -1; + private int originalColumnStart; + private int boundaryIndex; + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() != 1) { + return; + } + + Point point = e.getPoint(); + int rowIndex = point.y / rowHeight; + if (isOnOpenClose(rowIndex, point.x)) { + toggleOpen(rowIndex); + } + else { + + int columnIndex = getColumnIndex(rowIndex, point.x); + if (columnIndex >= 0) { + notifyCellClicked(rowIndex, columnIndex, e); + } + } + } + + private int getColumnIndex(int rowIndex, int x) { + int columnIndex = columnModel.getIndex(x); + if (columnIndex == 0) { + int indent = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH; + if (x < indent) { + return -1; + } + } + return columnIndex; + } + + private boolean isOnOpenClose(int rowIndex, int x) { + if (!rowModel.isExpandable(rowIndex)) { + return false; + } + int indent = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH; + return x >= indent && x < indent + ICON_WIDTH; + } + + @Override + public void mousePressed(MouseEvent e) { + Point p = e.getPoint(); + int rowIndex = p.y / rowHeight; + + if (e.getButton() == 1 && isOnOpenClose(rowIndex, p.x)) { + return; + } + + int index = findClosestColumnBoundary(e.getPoint().x); + + if (index >= 0) { + boundaryIndex = index; + startDragX = e.getPoint().x; + originalColumnStart = columnModel.getColumn(index).getStartX(); + return; + } + + if (DockingUtils.isControlModifier(e) && rowIndex == selectedRow) { + clearSelectedRow(); + } + else { + setSelectedRow(rowIndex); + } + } + + public int findClosestColumnBoundary(int x) { + for (int i = 1; i < columnModel.getColumnCount(); i++) { + GTrableColumn column = columnModel.getColumn(i); + int columnStart = column.getStartX(); + if (x > columnStart - TRIGGER_MARGIN && x < columnStart + TRIGGER_MARGIN) { + return i; + } + } + return -1; + } + + @Override + public void mouseMoved(MouseEvent e) { + int index = findClosestColumnBoundary(e.getPoint().x); + if (index >= 0) { + setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); + } + else { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + startDragX = -1; + boundaryIndex = -1; + } + + @Override + public void mouseDragged(MouseEvent e) { + if (startDragX < 0) { + return; + } + int x = e.getPoint().x; + int diff = x - startDragX; + int newColumnStart = originalColumnStart + diff; + columnModel.moveColumnStart(boundaryIndex, newColumnStart); + repaint(); + } + + } + + private class GTrableKeyListener extends KeyAdapter { + @Override + public void keyPressed(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_DOWN: + if (selectedRow < rowModel.getRowCount() - 1) { + setSelectedRow(selectedRow + 1); + scrollToSelectedRow(); + e.consume(); + } + break; + case KeyEvent.VK_UP: + if (selectedRow > 0) { + setSelectedRow(selectedRow - 1); + scrollToSelectedRow(); + e.consume(); + } + break; + case KeyEvent.VK_ENTER: + toggleOpen(selectedRow); + e.consume(); + break; + } + } + + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_DOWN: + if (selectedRow < rowModel.getRowCount() - 1) { + e.consume(); + } + break; + case KeyEvent.VK_UP: + if (selectedRow > 0) { + e.consume(); + } + break; + case KeyEvent.VK_ENTER: + e.consume(); + break; + } + } + + @Override + public void keyTyped(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_DOWN: + if (selectedRow < rowModel.getRowCount() - 1) { + e.consume(); + } + break; + case KeyEvent.VK_UP: + if (selectedRow > 0) { + e.consume(); + } + break; + case KeyEvent.VK_ENTER: + e.consume(); + break; + } + } + + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellClickedListener.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellClickedListener.java new file mode 100644 index 0000000000..dde0348c9a --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellClickedListener.java @@ -0,0 +1,32 @@ +/* ### + * 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 docking.widgets.trable; + +import java.awt.event.MouseEvent; + +/** + * Listener for {@link GTrable} cell clicked + */ +public interface GTrableCellClickedListener { + + /** + * Notification the a GTrable cell was clicked. + * @param row the row index of the cell that was clicked + * @param column the column index of the cell that was clicked + * @param event the mouse event of the click + */ + public void cellClicked(int row, int column, MouseEvent event); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellRenderer.java new file mode 100644 index 0000000000..ae7d21ada9 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableCellRenderer.java @@ -0,0 +1,40 @@ +/* ### + * 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 docking.widgets.trable; + +import java.awt.Component; + +/** + * Interface for {@link GTrable} cell renderers + * + * @param the type of the column value for this cell + */ +public interface GTrableCellRenderer { + + /** + * Gets and prepares the renderer component for the given column value + * @param trable the GTrable + * @param value the column value + * @param isSelected true if the row is selected + * @param hasFocus true if the cell has focus + * @param row the row of the cell being painted + * @param column the column of the cell being painted + * @return the component to use to paint the cell value + */ + public Component getCellRenderer(GTrable trable, C value, + boolean isSelected, boolean hasFocus, int row, int column); + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumn.java new file mode 100644 index 0000000000..65939c953d --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumn.java @@ -0,0 +1,76 @@ +/* ### + * 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 docking.widgets.trable; + +/** + * Abstract base class for {@link GTrable} column objects in the {@link GTrableColumnModel} + * + * @param the row object type + * @param the column value type + */ +public abstract class GTrableColumn { + private static final int DEFAULT_MIN_SIZE = 20; + private static GTrableCellRenderer DEFAULT_RENDERER = + new DefaultGTrableCellRenderer<>(); + + private int startX; + private int width; + + public GTrableColumn() { + width = getPreferredWidth(); + } + + public int getWidth() { + return width; + } + + @SuppressWarnings("unchecked") + public GTrableCellRenderer getRenderer() { + return (GTrableCellRenderer) DEFAULT_RENDERER; + } + + /** + * Returns the column value given the row object + * @param row the row object containing the data for the entire row + * @return the value to be displayed in this column + */ + public abstract C getValue(R row); + + protected int getPreferredWidth() { + return 100; + } + + void setWidth(int width) { + this.width = width; + } + + public int getMinWidth() { + return DEFAULT_MIN_SIZE; + } + + public boolean isResizable() { + return true; + } + + void setStartX(int x) { + this.startX = x; + } + + int getStartX() { + return startX; + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumnModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumnModel.java new file mode 100644 index 0000000000..78a68c97dc --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableColumnModel.java @@ -0,0 +1,252 @@ +/* ### + * 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 docking.widgets.trable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for {@link GTrable} column models + * + * @param the row object type + */ +public abstract class GTrableColumnModel { + private List> columns = new ArrayList<>(); + private int totalWidth; + + public GTrableColumnModel() { + reloadColumns(); + } + + /** + * {@return the number of columns in this model.} + */ + public int getColumnCount() { + return columns.size(); + } + + /** + * {@return the column object for the given column index.} + * @param column the index of the column + */ + public GTrableColumn getColumn(int column) { + return columns.get(column); + } + + /** + * {@return the preferred width of the model which is the sum of the preferred widths of each + * column.} + */ + public int getPreferredWidth() { + int preferredWidth = 0; + for (GTrableColumn column : columns) { + preferredWidth += column.getPreferredWidth(); + } + return preferredWidth; + } + + protected int computeWidth() { + int width = 0; + for (GTrableColumn column : columns) { + width += column.getWidth(); + } + return width; + } + + protected void reloadColumns() { + columns.clear(); + populateColumns(columns); + computeColumnStarts(); + totalWidth = computeWidth(); + + } + + /** + * Subclasses implement this method to define the columns for this model. + * @param columnList a list to populate with column objects + */ + protected abstract void populateColumns(List> columnList); + + protected void removeAllColumns() { + columns.removeAll(columns); + totalWidth = 0; + } + + protected int getWidth() { + return totalWidth; + } + + protected int getIndex(int x) { + for (int i = columns.size() - 1; i >= 0; i--) { + GTrableColumn column = columns.get(i); + if (x >= column.getStartX()) { + return i; + } + } + return 0; + } + + protected void setWidth(int newWidth) { + int diff = newWidth - totalWidth; + if (diff == 0) { + return; + } + if (diff > 0) { + int amount = growLeftPreferred(columns.size() - 1, diff); + growLeft(columns.size() - 1, amount); + } + else { + shrinkLeft(columns.size() - 1, -diff); + } + computeColumnStarts(); + } + + void moveColumnStart(int columnIndex, int x) { + GTrableColumn column = columns.get(columnIndex); + int currentStartX = column.getStartX(); + int diff = x - currentStartX; + if (diff > 0 && canGrowLeft(columnIndex - 1)) { + int actualAmount = shrinkRight(columnIndex, diff); + growLeft(columnIndex - 1, actualAmount); + } + else if (diff < 0 && canGrowRight(columnIndex)) { + int actualAmount = shrinkLeft(columnIndex - 1, -diff); + growRight(columnIndex, actualAmount); + } + computeColumnStarts(); + } + + private boolean canGrowLeft(int index) { + return canGrow(0, index); + } + + private boolean canGrowRight(int index) { + return canGrow(index, columns.size() - 1); + } + + private boolean canGrow(int index1, int index2) { + for (int i = index1; i <= index2; i++) { + if (columns.get(i).isResizable()) { + return true; + } + } + return false; + } + + private void computeColumnStarts() { + int x = 0; + for (int i = 0; i < columns.size(); i++) { + GTrableColumn column = columns.get(i); + column.setStartX(x); + int width = column.getWidth(); + x += width; + } + totalWidth = x; + modelColumnsChaged(); + } + + protected void modelColumnsChaged() { + // subclasses can override if they need to react to changes in the column positions or + // sizes + } + + private void growRight(int columnIndex, int amount) { + for (int i = columnIndex; i < columns.size(); i++) { + GTrableColumn column = columns.get(i); + if (column.isResizable()) { + column.setWidth(column.getWidth() + amount); + return; + } + } + } + + private void growLeft(int columnIndex, int amount) { + for (int i = columnIndex; i >= 0; i--) { + GTrableColumn column = columns.get(i); + if (column.isResizable()) { + column.setWidth(column.getWidth() + amount); + return; + } + } + } + + private int growLeftPreferred(int columnIndex, int amount) { + for (int i = columnIndex; i >= 0 && amount > 0; i--) { + GTrableColumn column = columns.get(i); + if (!column.isResizable()) { + continue; + } + int width = column.getWidth(); + int preferredWidth = column.getPreferredWidth(); + if (width < preferredWidth) { + int adjustment = Math.min(amount, preferredWidth - width); + column.setWidth(width + adjustment); + amount -= adjustment; + } + } + return amount; + } + + private int growRightPreferred(int columnIndex, int amount) { + for (int i = columnIndex; i < columns.size() && amount > 0; i++) { + GTrableColumn column = columns.get(i); + if (!column.isResizable()) { + continue; + } + int width = column.getWidth(); + int preferredWidth = column.getPreferredWidth(); + if (width < preferredWidth) { + int adjustment = Math.min(amount, preferredWidth - width); + column.setWidth(width + adjustment); + amount -= adjustment; + } + } + return amount; + } + + private int shrinkLeft(int columnIndex, int amount) { + int remainingAmount = amount; + for (int i = columnIndex; i >= 0 && remainingAmount > 0; i--) { + remainingAmount -= shrinkColumn(i, remainingAmount); + } + return amount - remainingAmount; + } + + private int shrinkRight(int columnIndex, int amount) { + int remainingAmount = amount; + for (int i = columnIndex; i < columns.size() && remainingAmount > 0; i++) { + remainingAmount -= shrinkColumn(i, remainingAmount); + } + return amount - remainingAmount; + } + + private int shrinkColumn(int columnIndex, int amount) { + GTrableColumn column = columns.get(columnIndex); + if (!column.isResizable()) { + return 0; + } + int currentWidth = column.getWidth(); + int minWidth = column.getMinWidth(); + + if (currentWidth >= minWidth + amount) { + column.setWidth(currentWidth - amount); + return amount; + } + column.setWidth(minWidth); + return currentWidth - minWidth; + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableModeRowlListener.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableModeRowlListener.java new file mode 100644 index 0000000000..84620d24d2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableModeRowlListener.java @@ -0,0 +1,27 @@ +/* ### + * 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 docking.widgets.trable; + +/** + * The listener interface for when the row model changes. + */ +public interface GTrableModeRowlListener { + + /** + * Notification that the row model changed + */ + public void trableChanged(); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRow.java new file mode 100644 index 0000000000..f387560302 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRow.java @@ -0,0 +1,66 @@ +/* ### + * 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 docking.widgets.trable; + +import java.util.List; + +/** + * Abstract base class for {@link GTrable} row objects. + * + * @param the row object type + */ +public abstract class GTrableRow> { + private final int indentLevel; + private boolean isExpanded = false; + + /** + * Constructor + * @param indentLevel the indent level of this row + */ + protected GTrableRow(int indentLevel) { + this.indentLevel = indentLevel; + } + + /** + * {@return the indent level for this row} + */ + public int getIndentLevel() { + return indentLevel; + } + + /** + * {@return true if this row is expandable} + */ + public abstract boolean isExpandable(); + + /** + * {@return true if this node is expanded.} + */ + public boolean isExpanded() { + return isExpanded; + } + + /** + * Sets the expanded state. + * @param expanded true if this row is expanded + */ + void setExpanded(boolean expanded) { + this.isExpanded = expanded; + } + + protected abstract List getChildRows(); + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRowModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRowModel.java new file mode 100644 index 0000000000..c7f5f9f020 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/GTrableRowModel.java @@ -0,0 +1,84 @@ +/* ### + * 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 docking.widgets.trable; + +/** + * Row model for a {@link GTrable}. + * + * @param the row object type + */ +public interface GTrableRowModel { + + /** + * {@return the total number of rows include open child rows.} + */ + public int getRowCount(); + + /** + * {@return the row object for the given index.} + * @param rowIndex the index of the row to retrieve + */ + public T getRow(int rowIndex); + + /** + * {@return true if the row at the given index can be expanded} + * @param rowIndex the row to test if expandable + */ + public boolean isExpandable(int rowIndex); + + /** + * {@return true if the row at the given index is expanded.} + * @param rowIndex the index of the row to test for expanded + */ + public boolean isExpanded(int rowIndex); + + /** + * Collapse the row at the given row index. + * @param rowIndex the index of the row to collapse + * @return the total number of rows removed due to collapsing the row + */ + public int collapseRow(int rowIndex); + + /** + * Expand the row at the given row index. + * @param rowIndex the index of the row to expand + * @return the total number of rows added due to the expand + */ + public int expandRow(int rowIndex); + + /** + * {@return the indent level of the row at the given index.} + * @param rowIndex the index of the row to get its indent level + */ + public int getIndentLevel(int rowIndex); + + /** + * Adds a listener to the list that is notified each time a change + * to the data model occurs. + * + * @param l the listener to be notified + */ + public void addListener(GTrableModeRowlListener l); + + /** + * Removes a listener from the list that is notified each time a + * change to the model occurs. + * + * @param l the listener to remove + */ + public void removeListener(GTrableModeRowlListener l); + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/OpenCloseIcon.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/OpenCloseIcon.java new file mode 100644 index 0000000000..951badff65 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/trable/OpenCloseIcon.java @@ -0,0 +1,105 @@ +/* ### + * 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 docking.widgets.trable; + +import java.awt.*; + +import javax.swing.Icon; + +import generic.theme.GThemeDefaults.Colors; + +/** + * Icon used for the expand/collapse control in a {@link GTrable} + */ +public class OpenCloseIcon implements Icon { + private int width; + private int height; + private int[] xPoints; + private int[] yPoints; + private Color color = Colors.FOREGROUND; + + /** + * Constructor + * @param isOpen if true, draws an icon that indicates the row is open, otherwise draws an + * icon that the icon indicates the row is closed + * @param width the width to draw the icon + * @param height the height to draw the icon + */ + public OpenCloseIcon(boolean isOpen, int width, int height) { + this.width = width; + this.height = height; + if (isOpen) { + buildDownPointingTriangle(); + } + else { + buildRightPointingTriangle(); + } + } + + public void setColor(Color color) { + this.color = color; + } + + private void buildDownPointingTriangle() { + int triangleWidth = 8; + int triangleHeight = 4; + + int startX = width / 2 - triangleWidth / 2; + int endX = startX + triangleWidth; + + int startY = height / 2 - triangleHeight / 2; + int endY = startY + triangleHeight; + + xPoints = new int[] { startX, endX, (startX + endX) / 2 }; + yPoints = new int[] { startY, startY, endY }; + + } + + private void buildRightPointingTriangle() { + int triangleWidth = 4; + int triangleHeight = 8; + + int startX = width / 2 - triangleWidth / 2; + int endX = startX + triangleWidth; + + int startY = height / 2 - triangleHeight / 2; + int endY = startY + triangleHeight; + + xPoints = new int[] { startX, endX, startX }; + yPoints = new int[] { startY, (startY + endY) / 2, endY }; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + g.setColor(color); + g.translate(x, y); + Graphics2D graphics2D = (Graphics2D) g; + graphics2D.drawPolygon(xPoints, yPoints, 3); + graphics2D.fillPolygon(xPoints, yPoints, 3); + g.translate(-x, -y); + } + + @Override + public int getIconWidth() { + return width; + } + + @Override + public int getIconHeight() { + return height; + } + +} diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/trable/GTrableTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/trable/GTrableTest.java new file mode 100644 index 0000000000..4a6aa6c930 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/trable/GTrableTest.java @@ -0,0 +1,338 @@ +/* ### + * 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 docking.widgets.trable; + +import static org.junit.Assert.*; + +import java.util.*; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGuiTest; +import ghidra.util.datastruct.Range; + +public class GTrableTest extends AbstractGuiTest { + + private GTrableRowModel rowModel; + private TestColumnModel columnModel; + private GTrable gTrable; + private JFrame frame; + + @Before + public void setUp() { + rowModel = createRowModel(); + columnModel = new TestColumnModel(); + gTrable = new GTrable(rowModel, columnModel); + gTrable.setPreferredVisibleRowCount(3, 3); + JScrollPane scroll = new JScrollPane(gTrable); + frame = new JFrame("Test"); + frame.getContentPane().add(scroll); + frame.pack(); + frame.setVisible(true); + } + + @Test + public void testInitialState() { + + //@formatter:off + assertAllRows( + "a", + "b", + "c" + ); + assertVisibleRows( + "a", + "b", + "c" + ); + //@formatter:on + + Range visibleRows = gTrable.getVisibleRows(); + assertEquals(0, visibleRows.min); + assertEquals(2, visibleRows.max); + + assertTrue(rowModel.getRow(0).isExpandable()); + assertFalse(rowModel.getRow(1).isExpandable()); + assertTrue(rowModel.getRow(2).isExpandable()); + } + + @Test + public void testExpandRow() { + selectRow(1); + //@formatter:off + assertVisibleRows( + "a", + "b", + "c" + ); + //@formatter:on + + assertTrue(rowModel.getRow(0).isExpandable()); + expandRow(0); + + //@formatter:off + assertVisibleRows( + "a", + " a.1", + " a.2" + ); + assertAllRows( + "a", + " a.1", + " a.2", + " a.3", + "b", + "c" + ); + //@formatter:on + + assertEquals(4, gTrable.getSelectedRow()); + } + + @Test + public void testCollapseRow() { + expandRow(0); + selectRow(5); + + //@formatter:off + assertVisibleRows( + "a", + " a.1", + " a.2" + ); + assertAllRows( + "a", + " a.1", + " a.2", + " a.3", + "b", + "c" + ); + //@formatter:on + + assertTrue(rowModel.isExpanded(0)); + + collapseRow(0); + + //@formatter:off + assertVisibleRows( + "a", + "b", + "c" + ); + assertAllRows( + "a", + "b", + "c" + ); + //@formatter:on + assertEquals(2, gTrable.getSelectedRow()); + } + + @Test + public void testExpandAllRow() { + + //@formatter:off + assertVisibleRows( + "a", + "b", + "c" + ); + //@formatter:on + + expandAll(); + + //@formatter:off + assertVisibleRows( + "a", + " a.1", + " a.2" + ); + assertAllRows( + "a", + " a.1", + " a.2", + " a.2.A", + " a.2.B", + " a.2.C", + " a.3", + "b", + "c", + " c.1", + " c.2" + ); + //@formatter:on + } + + @Test + public void testScrollToSelectedRow() { + expandAll(); + selectRow(5); + + //@formatter:off + assertVisibleRows( + "a", + " a.1", + " a.2" + ); + //@formatter:on + + scrollToSelectedRow(); + + //@formatter:off + assertVisibleRows( + " a.2.A", + " a.2.B", + " a.2.C" + ); + //@formatter:on + + } + + private void scrollToSelectedRow() { + runSwing(() -> { + gTrable.scrollToSelectedRow(); + }); + waitForSwing(); + } + + private void expandRow(int row) { + runSwing(() -> { + gTrable.expandRow(row); + }); + waitForSwing(); + } + + private void collapseRow(int row) { + runSwing(() -> { + gTrable.collapseRow(row); + }); + waitForSwing(); + } + + private void expandAll() { + runSwing(() -> { + gTrable.expandAll(); + }); + waitForSwing(); + } + + private void selectRow(int row) { + runSwing(() -> { + gTrable.setSelectedRow(row); + }); + } + + private void assertAllRows(String... expectedRows) { + List actualRows = getRowsAsText(0, rowModel.getRowCount() - 1); + assertEquals(expectedRows.length, actualRows.size()); + List expectedList = Arrays.asList(expectedRows); + assertListEqualOrdered(expectedList, actualRows); + } + + private void assertVisibleRows(String... expectedRows) { + Range visibleRows = gTrable.getVisibleRows(); + List actualRows = getRowsAsText(visibleRows.min, visibleRows.max); + assertEquals(expectedRows.length, actualRows.size()); + List expectedList = Arrays.asList(expectedRows); + assertListEqualOrdered(expectedList, actualRows); + } + + private List getRowsAsText(int startRow, int endRow) { + List list = new ArrayList<>(); + for (int i = startRow; i <= endRow; i++) { + TestDataRow row = rowModel.getRow(i); + int indent = row.getIndentLevel(); + String name = row.getName(); + String indentation = StringUtils.repeat("\t", indent); + list.add(indentation + name); + } + return list; + } + + private GTrableRowModel createRowModel() { + TestDataRow a2A = new TestDataRow("a.2.A", 2, null); + TestDataRow a2B = new TestDataRow("a.2.B", 2, null); + TestDataRow a2C = new TestDataRow("a.2.C", 2, null); + + TestDataRow a1 = new TestDataRow("a.1", 1, null); + TestDataRow a2 = new TestDataRow("a.2", 1, List.of(a2A, a2B, a2C)); + TestDataRow a3 = new TestDataRow("a.3", 1, null); + + TestDataRow c1 = new TestDataRow("c.1", 1, null); + TestDataRow c2 = new TestDataRow("c.2", 1, null); + + TestDataRow a = new TestDataRow("a", 0, List.of(a1, a2, a3)); + TestDataRow b = new TestDataRow("b", 0, null); + TestDataRow c = new TestDataRow("c", 0, List.of(c1, c2)); + + return new DefaultGTrableRowModel<>(List.of(a, b, c)); + } + + class TestDataRow extends GTrableRow { + + private List children; + private String name; + + protected TestDataRow(String name, int indentLevel, List children) { + super(indentLevel); + this.name = name; + this.children = children; + } + + public String getName() { + return name; + } + + @Override + public boolean isExpandable() { + return children != null; + } + + @Override + protected List getChildRows() { + return children; + } + + } + + private class NameColumn extends GTrableColumn { + @Override + public String getValue(TestDataRow row) { + return row.getName(); + } + + @Override + protected int getPreferredWidth() { + return 150; + } + + } + + class TestColumnModel extends GTrableColumnModel { + + @Override + protected void populateColumns(List> columnList) { + columnList.add(new NameColumn()); + } + + } +} diff --git a/Ghidra/Framework/Graph/build.gradle b/Ghidra/Framework/Graph/build.gradle index e35ba7b4b9..254d5372d1 100644 --- a/Ghidra/Framework/Graph/build.gradle +++ b/Ghidra/Framework/Graph/build.gradle @@ -4,9 +4,9 @@ * 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. @@ -15,6 +15,7 @@ */ apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle" apply from: "$rootProject.projectDir/gradle/javaProject.gradle" +apply from: "$rootProject.projectDir/gradle/helpProject.gradle" apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle" apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle" apply from: "$rootProject.projectDir/gradle/javadoc.gradle" diff --git a/Ghidra/Framework/Graph/certification.manifest b/Ghidra/Framework/Graph/certification.manifest index d5341d8f88..ac0a67c52c 100644 --- a/Ghidra/Framework/Graph/certification.manifest +++ b/Ghidra/Framework/Graph/certification.manifest @@ -14,6 +14,8 @@ src/main/docs/VisualGraphHierarchy.png||GHIDRA||||END| src/main/docs/VisualGraphHierarchy.xml||GHIDRA||||END| src/main/docs/VisualGraphViewer.png||GHIDRA||||END| src/main/docs/VisualGraphViewer.xml||GHIDRA||||END| +src/main/help/help/TOC_Source.xml||GHIDRA||||END| +src/main/help/help/topics/VisualGraph/Visual_Graph.html||GHIDRA||||END| src/main/resources/images/Lasso.png||GHIDRA||||END| src/main/resources/images/color_swatch.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/network-wireless-16.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| diff --git a/Ghidra/Framework/Graph/src/main/help/help/TOC_Source.xml b/Ghidra/Framework/Graph/src/main/help/help/TOC_Source.xml new file mode 100644 index 0000000000..8a327c6107 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/help/help/TOC_Source.xml @@ -0,0 +1,54 @@ + + + + + + + + diff --git a/Ghidra/Framework/Graph/src/main/help/help/topics/VisualGraph/Visual_Graph.html b/Ghidra/Framework/Graph/src/main/help/help/topics/VisualGraph/Visual_Graph.html new file mode 100644 index 0000000000..c2356bba25 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/help/help/topics/VisualGraph/Visual_Graph.html @@ -0,0 +1,149 @@ + + + + + + + Visual Graphs + + + + + +

Visual Graphs

+ +
+

Visual Graphs are highly integrated graphs that all share common features and + actions. They typically have both a Primary View and + a Satellite View + +

+ +

Primary View

+ +
+

The primary view is the main way to view and interact with the graph whose vertices + and edges are specialized for the particular visual graph instance.

+
+ +

Satellite View

+ +
+

The Satellite View provides an overview of the graph. From this view you may also perform + basic adjustment of the overall graph location. In addition to the complete graph, the + satellite view contains a lens (the white rectangle) that indicates how much of the + current graph fits into the primary view.

+ +

The Satellite View can be in one of three states: docked, undocked, or not showing. A + docked satellite view will be display in a corner of the primary graph. An undocked view + will be displayed in its own window. If the view is not showing, there will be an + icon in the corner to indicate that + it is available.

+ +

When you left-click in the satellite view the graph is centered around the + corresponding point in the primary view. Alternatively, you may drag the lens of the + satellite view to the desired location by performing a mouse drag operation on the lens.

+ +

You may show/hide the satellite view by right-clicking anywhere in the Primary View and + selecting or deselecting the Display Satellite View toggle button from the popup + menu.

+ +
+

If the Primary View is painting + sluggishly, then hiding the Satellite View cause the Primary View to be more + responsive.

+
+ +

Detached Satellite

+ +
+

The Satellite View is docked by default. However, you can choose to instead + undock it and display it in its own window.

+ +

To undock the Satellite View, right-click in the graph and deselect the Dock + Satellite View menu item.

+ +

To re-dock the Satellite View, right-click in the graph and select the Dock Satellite + View menu item.

+ +
+

To show the Satellite View if it is + hidden, whether docked or undocked, you can press the button. This button is in the lower-right + hand corner of the graph and is only visible if the Satellite View is hidden or + undocked.

+
+
+ +

Docked Satellite Location

+ +
+

When the Satellite View is attached, or docked, to the Primary View, you can + choose which corner to show the satellite view. To change the corner, right-click in the + graph, select Docked Satellite Position and then select the appropriate sub-menu for + the desired corner.

+
+
+ +

Standar Graph Operations

+ +
+

Panning

+ +
+

There are various ways to move the graph. To move the graph in any direction you can + drag from the whitespace of the graph.

+ +

By default, the scroll wheel zooms the graph. The scroll wheel can also be used + to scroll the graph vertically by holding the Ctrl key while + using the scroll wheel. Alternatively, you can move the graph left to right using the + mouse while holding Ctrl-Alt.

+ +

The satellite viewer may also be used to move the primary graphs view by dragging and + clicking inside of the satellite viewer.

+
+ +

Zooming

+ +
+

At full zoom, or block level zoom, each block is rendered at its natural + size. From that point, which is a + 1:1 zoom level, you can zoom out in order to fit more of the graph into the display.

+ +
+

To change the zoom you may use the mouse scroll wheel. This works + whether the mouse is over the primary viewer or the satellite viewer.

+ +
+ +

The satellite viewer is always zoomed out far enough to + fit the entire graph into its window.

+
+ +

Interaction Threshold

+ +
+

While zooming out (away from the vertices) you will eventually reach a point where you + can no longer interact with the component inside of the block. The blocks provide a subtle + visual indication when they are zoomed past this level, in the form of a drop-shadow. The + image below shows this drop-shadow. The block on the left is not past the interaction + threshold, but the block on the right is, and thus has a drop-shadow. This example is for + illustrative purposes only and during normal usage all blocks will share the save zoom + level. So, if one block is zoomed past the interaction threshold, all other blocks will + be as well.

+
+ +

Painting Threshold

+ +
+

While zooming out (away from the blocks) you will eventually reach a point where + contents each vertex will not be painted. Instead, each block will be painted by a + rectangle that is painted with the current background color of the vertex.

+
+
+ +
+
+ + diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java index f15ff79a87..312278d060 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java @@ -4,9 +4,9 @@ * 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. @@ -26,6 +26,7 @@ import ghidra.framework.options.SaveState; import ghidra.graph.featurette.VgSatelliteFeaturette; import ghidra.graph.featurette.VisualGraphFeaturette; import ghidra.graph.viewer.*; +import ghidra.graph.viewer.GraphComponent.SatellitePosition; import ghidra.graph.viewer.actions.*; import ghidra.graph.viewer.event.mouse.VertexMouseInfo; @@ -137,11 +138,16 @@ public abstract class VisualGraphComponentProvider satelliteFeature = new VgSatelliteFeaturette<>(); satelliteFeature.init(this); subFeatures.add(satelliteFeature); + satelliteFeature.setSatellitePosition(position); + satelliteFeature.setSatelliteVisible(satelliteVisible); } - /* Features to provide diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/featurette/VgSatelliteFeaturette.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/featurette/VgSatelliteFeaturette.java index e562ac76fb..3af7078d4b 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/featurette/VgSatelliteFeaturette.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/featurette/VgSatelliteFeaturette.java @@ -97,27 +97,11 @@ public class VgSatelliteFeaturette> + extends RelayoutFunctionGraphJob { + private V vertex; + private Point2D destination; + private Point2D lastPoint = new Point2D.Double(0, 0); + + public RelayoutAndCenterVertexGraphJob(GraphViewer viewer, V vertex, + boolean useAnimation) { + super(viewer, useAnimation); + this.vertex = vertex; + } + + @Override + protected void initializeVertexLocations() { + super.initializeVertexLocations(); + TransitionPoints transitionPoints = vertexLocations.get(vertex); + Point2D centerPoint = transitionPoints.destinationPoint; + Point p = new Point((int) centerPoint.getX(), (int) centerPoint.getY()); + destination = GraphViewerUtils.getOffsetFromCenterInLayoutSpace(viewer, p); + } + + @Override + public void setPercentComplete(double percentComplete) { + super.setPercentComplete(percentComplete); + + double finalX = destination.getX(); + double finalY = destination.getY(); + + double lastX = lastPoint.getX(); + double lastY = lastPoint.getY(); + double deltaX = (percentComplete * finalX) - lastX; + double deltaY = (percentComplete * finalY) - lastY; + + lastPoint.setLocation(lastX + deltaX, lastY + deltaY); + + if (deltaX == 0 && deltaY == 0) { + return; + } + + RenderContext renderContext = viewer.getRenderContext(); + MultiLayerTransformer xform = renderContext.getMultiLayerTransformer(); + xform.getTransformer(Layer.LAYOUT).translate(deltaX, deltaY); + viewer.repaint(); + + } + + @Override + protected void finished() { + if (isShortcut) { + destination = GraphViewerUtils.getVertexOffsetFromLayoutCenter(viewer, vertex); + } + super.finished(); + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutAndEnsureVisible.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutAndEnsureVisible.java new file mode 100644 index 0000000000..48090a920d --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutAndEnsureVisible.java @@ -0,0 +1,166 @@ +/* ### + * 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.job; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.Point2D; + +import edu.uci.ics.jung.visualization.*; +import ghidra.graph.viewer.*; + +/** + * Graph job to move the entire graph to ensure one or two vertices are fully on screen. If both + * vertices can't be fully shown at the same time, the primary vertex gets precedence. + * + * @param the vertex type + * @param the edge type + */ +public class RelayoutAndEnsureVisible> + extends RelayoutFunctionGraphJob { + private static final int VIEW_BOUNDARY_PADDING = 50; + private V primaryVertex; + private V secondaryVertex; + private Distance moveDistance; + private Distance movedSoFar = new Distance(0, 0); + + public RelayoutAndEnsureVisible(GraphViewer viewer, V primaryVertex, V secondaryVertex, + boolean useAnimation) { + super(viewer, useAnimation); + this.primaryVertex = primaryVertex; + this.secondaryVertex = secondaryVertex; + } + + @Override + protected void initializeVertexLocations() { + super.initializeVertexLocations(); + + Shape layoutViewerShape = + GraphViewerUtils.translateShapeFromViewSpaceToLayoutSpace(viewer.getBounds(), viewer); + Rectangle layoutViewerBounds = layoutViewerShape.getBounds(); + + // get layout destination position for each vertex + Rectangle primaryLayoutVertexBounds = + GraphViewerUtils.getVertexBoundsInLayoutSpace(viewer, primaryVertex); + Rectangle secondaryLayoutVertexBounds = + GraphViewerUtils.getVertexBoundsInLayoutSpace(viewer, secondaryVertex); + + setRectangleLocationToFinalDestination(primaryLayoutVertexBounds, primaryVertex); + setRectangleLocationToFinalDestination(secondaryLayoutVertexBounds, secondaryVertex); + + padVertexBounds(primaryLayoutVertexBounds); + padVertexBounds(secondaryLayoutVertexBounds); + + // This is the distance we need to move the view to ensure the less important vertex is + // is fully visible in the view. + Distance secondaryMoveDistance = + getMoveDistanceToContainVertexInView(layoutViewerBounds, secondaryLayoutVertexBounds); + + // Assuming we already moved the layout as computed above, how much additional movement is + // needed to bring the preferred vertex fully into view. Note that if both vertices don't + // fit, the secondary vertex may no longer be visible after all movement is applied. + layoutViewerBounds.x -= secondaryMoveDistance.deltaX; + layoutViewerBounds.y -= secondaryMoveDistance.deltaY; + Distance primaryMoveDistance = + getMoveDistanceToContainVertexInView(layoutViewerBounds, primaryLayoutVertexBounds); + + // The total distance we need to move the view is the net effect of combining the first + // move and the second move. + moveDistance = secondaryMoveDistance.add(primaryMoveDistance); + } + + private Distance getMoveDistanceToContainVertexInView( + Rectangle layoutViewerBounds, Rectangle layoutVertexBounds) { + + // if the vertex is already fully in the view, no move needed + if (layoutViewerBounds.contains(layoutVertexBounds)) { + return new Distance(0, 0); + } + + int deltaX = 0; + int deltaY = 0; + + int view1x = layoutViewerBounds.x; + int view1y = layoutViewerBounds.y; + + int view2x = layoutViewerBounds.x + layoutViewerBounds.width; + int view2y = layoutViewerBounds.y + layoutViewerBounds.height; + + int vertex1x = layoutVertexBounds.x; + int vertex1y = layoutVertexBounds.y; + + int vertex2x = layoutVertexBounds.x + layoutVertexBounds.width; + int vertex2y = layoutVertexBounds.y + layoutVertexBounds.height; + + if (view1x > vertex1x) { + deltaX = -(vertex1x - view1x); + } + else if (view2x < vertex2x) { + deltaX = -(vertex2x - view2x); + } + + if (view1y > vertex1y) { + deltaY = -(vertex1y - view1y); + } + else if (view2y < vertex2y) { + deltaY = -(vertex2y - view2y); + + } + return new Distance(deltaX, deltaY); + } + + private void padVertexBounds(Rectangle layoutVertexBounds) { + layoutVertexBounds.x -= VIEW_BOUNDARY_PADDING; + layoutVertexBounds.y -= VIEW_BOUNDARY_PADDING; + layoutVertexBounds.width += 2 * VIEW_BOUNDARY_PADDING; + layoutVertexBounds.height += 2 * VIEW_BOUNDARY_PADDING; + } + + private void setRectangleLocationToFinalDestination(Rectangle layoutVertexBounds, V v) { + TransitionPoints transitionPoints = vertexLocations.get(v); + Point2D centerPoint = transitionPoints.destinationPoint; + int upperLeftCornerX = (int) centerPoint.getX() - layoutVertexBounds.width / 2; + int upperLeftCornerY = (int) centerPoint.getY() - layoutVertexBounds.height / 2; + layoutVertexBounds.setLocation(upperLeftCornerX, upperLeftCornerY); + } + + @Override + public void setPercentComplete(double percentComplete) { + super.setPercentComplete(percentComplete); + + Distance newMovedSoFar = moveDistance.scale(percentComplete); + double deltaX = newMovedSoFar.deltaX - movedSoFar.deltaX; + double deltaY = newMovedSoFar.deltaY - movedSoFar.deltaY; + + RenderContext renderContext = viewer.getRenderContext(); + MultiLayerTransformer xform = renderContext.getMultiLayerTransformer(); + xform.getTransformer(Layer.LAYOUT).translate(deltaX, deltaY); + viewer.repaint(); + + movedSoFar = newMovedSoFar; + } + + private record Distance(int deltaX, int deltaY) { + Distance scale(double scale) { + return new Distance((int) (deltaX * scale), (int) (deltaY * scale)); + } + + public Distance add(Distance other) { + return new Distance(deltaX + other.deltaX, deltaY + other.deltaY); + } + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutFunctionGraphJob.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutFunctionGraphJob.java index b006a6c56d..daed15fcef 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutFunctionGraphJob.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/job/RelayoutFunctionGraphJob.java @@ -4,9 +4,9 @@ * 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. @@ -19,9 +19,6 @@ import java.awt.geom.Point2D; import java.util.*; import java.util.Map.Entry; -import org.jdesktop.animation.timing.Animator; -import org.jdesktop.animation.timing.interpolation.PropertySetter; - import ghidra.graph.viewer.*; import ghidra.graph.viewer.layout.LayoutPositions; @@ -32,25 +29,6 @@ public class RelayoutFunctionGraphJob vertices = graph.getVertices(); + for (V vertex : vertices) { Point2D currentPoint = toLocation(vertex); Point2D startPoint = (Point2D) currentPoint.clone(); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java index 91e3af9348..8673ab7747 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java @@ -293,7 +293,7 @@ public class GraphComponent, G e // the layout defines the shape of the edge (this gives the layout flexibility in how // to render its shape) - Function edgeTransformer = layout.getEdgeShapeTransformer(); + Function edgeTransformer = layout.getEdgeShapeTransformer(renderContext); renderContext.setEdgeShapeTransformer(edgeTransformer); renderContext.setArrowPlacementTolerance(5.0f); @@ -361,7 +361,7 @@ public class GraphComponent, G e visualEdgeRenderer.setHoveredColorTransformer( e -> new GColor("color.visualgraph.view.satellite.edge.hovered")); - Function edgeTransformer = layout.getEdgeShapeTransformer(); + Function edgeTransformer = layout.getEdgeShapeTransformer(renderContext); renderContext.setEdgeShapeTransformer(edgeTransformer); renderContext.setVertexShapeTransformer(new VisualGraphVertexShapeTransformer<>()); @@ -471,14 +471,9 @@ public class GraphComponent, G e button.setOpaque(false); button.setToolTipText(tooltip); - /* - - TODO fix when the Generic Visual Graph help module is created - HelpService helpService = DockingWindowManager.getHelpService(); helpService.registerHelp(button, - new HelpLocation("GraphTopic", "Satellite_View_Dock")); - */ + new HelpLocation("Visual_Graph", "Satellite_View_Dock")); return button; } @@ -1011,7 +1006,7 @@ public class GraphComponent, G e v.setLocation(newLocation); - if (changeType == ChangeType.RESTORE) { + if (changeType.isTransitional()) { // ignore these events, as they are a bulk operation and will be handled later return; } @@ -1183,6 +1178,12 @@ public class GraphComponent, G e @Override public void verticesRemoved(Iterable vertices) { getPathHighlighter().clearEdgeCache(); + + // clear any deleted nodes from the pick state + PickedState pickedState = primaryViewer.getPickedVertexState(); + for (V v : vertices) { + pickedState.pick(v, false); + } } @Override diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewerUtils.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewerUtils.java index 17ea317fe6..14d864d9cc 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewerUtils.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewerUtils.java @@ -117,9 +117,15 @@ public class GraphViewerUtils { public static Point getVertexUpperLeftCornerInLayoutSpace( VisualizationServer viewer, V vertex) { + Point vertexCenterInLayoutSpace = getVertexCenterPointInLayoutSpace(viewer, vertex); - Point vertexGraphSpaceLocation = getVertexUpperLeftCornerInGraphSpace(viewer, vertex); - return translatePointFromGraphSpaceToLayoutSpace(vertexGraphSpaceLocation, viewer); + RenderContext renderContext = viewer.getRenderContext(); + Shape shape = renderContext.getVertexShapeTransformer().apply(vertex); + Rectangle shapeBounds = shape.getBounds(); + Point vertexUpperLeftPointRelativeToVertexCenter = shapeBounds.getLocation(); + + return new Point(vertexCenterInLayoutSpace.x + vertexUpperLeftPointRelativeToVertexCenter.x, + vertexCenterInLayoutSpace.y + vertexUpperLeftPointRelativeToVertexCenter.y); } public static Point getVertexUpperLeftCornerInViewSpace(VisualizationServer viewer, @@ -215,17 +221,8 @@ public class GraphViewerUtils { public static Point getVertexUpperLeftCornerInGraphSpace( VisualizationServer viewer, V vertex) { - Point vertexCenterInLayoutSpace = getVertexCenterPointInLayoutSpace(viewer, vertex); - Point vertexCenterInGraphSpace = - translatePointFromLayoutSpaceToGraphSpace(vertexCenterInLayoutSpace, viewer); - - RenderContext renderContext = viewer.getRenderContext(); - Shape shape = renderContext.getVertexShapeTransformer().apply(vertex); - Rectangle shapeBounds = shape.getBounds(); - Point vertexUpperLeftPointRelativeToVertexCenter = shapeBounds.getLocation(); - - return new Point(vertexCenterInGraphSpace.x + vertexUpperLeftPointRelativeToVertexCenter.x, - vertexCenterInGraphSpace.y + vertexUpperLeftPointRelativeToVertexCenter.y); + Point layoutPoint = getVertexUpperLeftCornerInLayoutSpace(viewer, vertex); + return translatePointFromLayoutSpaceToGraphSpace(layoutPoint, viewer); } public static Point translatePointFromLayoutSpaceToGraphSpace(Point2D pointInLayoutSpace, @@ -688,32 +685,11 @@ public class GraphViewerUtils { double endY = (float) endVertexCenter.getY(); RenderContext renderContext = viewer.getRenderContext(); - if (isLoop) { - // - // Our edge loops are sized and positioned according to the shared - // code in the utils class. We do this so that our hit detection matches our rendering. - // - Function vertexShapeTransformer = - renderContext.getVertexShapeTransformer(); - Shape vertexShape = getVertexShapeForEdge(endVertex, vertexShapeTransformer); - return createHollowEgdeLoopInGraphSpace(vertexShape, startX, startY); - } // translate the edge from 0,0 to the starting vertex point AffineTransform xform = AffineTransform.getTranslateInstance(startX, startY); Shape edgeShape = renderContext.getEdgeShapeTransformer().apply(e); - double deltaX = endX - startX; - double deltaY = endY - startY; - - // rotate the edge to the angle between the vertices - double theta = Math.atan2(deltaY, deltaX); - xform.rotate(theta); - - // stretch the edge to span the distance between the vertices - double dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - xform.scale(dist, 1.0f); - // apply the transformations; converting the given shape from model space into graph space return xform.createTransformedShape(edgeShape); } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VertexMouseInfo.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VertexMouseInfo.java index f0f126e49c..1f2af0da30 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VertexMouseInfo.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VertexMouseInfo.java @@ -4,9 +4,9 @@ * 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. @@ -17,6 +17,7 @@ package ghidra.graph.viewer.event.mouse; import java.awt.*; import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; import java.awt.geom.Point2D; import java.util.Objects; @@ -46,9 +47,11 @@ public class VertexMouseInfo> { private final V vertex; private MouseEvent translatedMouseEvent; private Component mousedDestinationComponent; + private Point2D vertexBasedClickPoint; public VertexMouseInfo(MouseEvent originalMouseEvent, V vertex, Point2D vertexBasedClickPoint, GraphViewer viewer) { + this.vertexBasedClickPoint = vertexBasedClickPoint; this.originalMouseEvent = Objects.requireNonNull(originalMouseEvent); this.vertex = Objects.requireNonNull(vertex); this.viewer = Objects.requireNonNull(viewer); @@ -59,6 +62,10 @@ public class VertexMouseInfo> { setClickedComponent(deepestComponent, vertexBasedClickPoint); } + public Point2D getVertexRelativeClickPoint() { + return vertexBasedClickPoint; + } + public boolean isScaledPastInteractionThreshold() { RenderContext renderContext = viewer.getRenderContext(); MultiLayerTransformer multiLayerTransformer = renderContext.getMultiLayerTransformer(); @@ -75,6 +82,11 @@ public class VertexMouseInfo> { if (!isVertexSelected()) { return HAND_CURSOR; } + + if (mousedDestinationComponent != null) { + return mousedDestinationComponent.getCursor(); + } + return DEFAULT_CURSOR; } @@ -221,12 +233,22 @@ public class VertexMouseInfo> { System.currentTimeMillis(), 0, 0, 0, 0, false); } - private MouseEvent createMouseEventFromSource(Component source, MouseEvent progenitor, + private MouseEvent createMouseEventFromSource(Component source, MouseEvent ev, Point2D clickPoint) { - return new MouseEvent(source, progenitor.getID(), progenitor.getWhen(), - progenitor.getModifiers() | progenitor.getModifiersEx(), (int) clickPoint.getX(), - (int) clickPoint.getY(), progenitor.getClickCount(), progenitor.isPopupTrigger(), - progenitor.getButton()); + if (ev instanceof MouseWheelEvent wheelEvent) { + int scrollType = wheelEvent.getScrollType(); + int scrollAmount = wheelEvent.getScrollAmount(); + int wheelRotation = wheelEvent.getWheelRotation(); + return new MouseWheelEvent(source, ev.getID(), ev.getWhen(), + ev.getModifiers() | ev.getModifiersEx(), (int) clickPoint.getX(), + (int) clickPoint.getY(), ev.getClickCount(), ev.isPopupTrigger(), + scrollType, scrollAmount, wheelRotation); + } + + return new MouseEvent(source, ev.getID(), ev.getWhen(), + ev.getModifiers() | ev.getModifiersEx(), (int) clickPoint.getX(), + (int) clickPoint.getY(), ev.getClickCount(), ev.isPopupTrigger(), + ev.getButton()); } public boolean isPopupClick() { diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEventForwardingGraphMousePlugin.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEventForwardingGraphMousePlugin.java index e19320608f..e2af46aebe 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEventForwardingGraphMousePlugin.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphEventForwardingGraphMousePlugin.java @@ -4,9 +4,9 @@ * 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. @@ -265,6 +265,7 @@ public class VisualGraphEventForwardingGraphMousePlugin currentInfo, diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPickingGraphMousePlugin.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPickingGraphMousePlugin.java index d05fcb040b..e1b3a0297b 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPickingGraphMousePlugin.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPickingGraphMousePlugin.java @@ -4,9 +4,9 @@ * 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. @@ -28,8 +28,11 @@ import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor; import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.visualization.*; +import edu.uci.ics.jung.visualization.layout.ObservableCachingLayout; import edu.uci.ics.jung.visualization.picking.PickedState; import ghidra.graph.viewer.*; +import ghidra.graph.viewer.layout.LayoutListener.ChangeType; +import ghidra.graph.viewer.layout.VisualGraphLayout; public class VisualGraphPickingGraphMousePlugin> extends JungPickingGraphMousePlugin implements VisualGraphMousePlugin { @@ -100,7 +103,13 @@ public class VisualGraphPickingGraphMousePlugin vgLayout = getVisualGraphLayout(layout); + if (vgLayout != null) { + vgLayout.setLocation(v, vertexPoint, ChangeType.USER); + } + else { + layout.setLocation(v, vertexPoint); + } updatedArticulatedEdges(viewer, v); } @@ -108,6 +117,16 @@ public class VisualGraphPickingGraphMousePlugin getVisualGraphLayout(Layout layout) { + if (layout instanceof VisualGraphLayout vgLayout) { + return vgLayout; + } + if (layout instanceof ObservableCachingLayout observable) { + return getVisualGraphLayout(observable.getDelegate()); + } + return null; + } + private void updatedArticulatedEdges(GraphViewer viewer, V v) { Layout layout = viewer.getGraphLayout(); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPluggableGraphMouse.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPluggableGraphMouse.java index e8f92dbc8c..be93e37782 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPluggableGraphMouse.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/event/mouse/VisualGraphPluggableGraphMouse.java @@ -4,9 +4,9 @@ * 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. @@ -15,6 +15,7 @@ */ package ghidra.graph.viewer.event.mouse; +import java.awt.Component; import java.awt.event.*; import java.util.concurrent.CopyOnWriteArrayList; @@ -113,14 +114,15 @@ public class VisualGraphPluggableGraphMouse + * This pluggable graph mouse sub-system will stop processing when one of the plugins consumes + * the mouse event. We have to create a copy to avoid an already consumed incoming event from + * short-circuiting our event processing. + * @param e + * @return a copy if the original incoming event with the consumed flag cleared. + */ + private MouseEvent copy(MouseEvent e) { + Component source = e.getComponent(); + int id = e.getID(); + int button = e.getButton(); + long when = e.getWhen(); + int modifiers = e.getModifiersEx(); + int x = e.getX(); + int y = e.getY(); + int clickCount = e.getClickCount(); + boolean popupTrigger = e.isPopupTrigger(); + return new MouseEvent(source, id, when, modifiers, x, y, clickCount, popupTrigger, button); + } + + private MouseWheelEvent copy(MouseWheelEvent e) { + Component source = e.getComponent(); + int id = e.getID(); + long when = e.getWhen(); + int modifiers = e.getModifiersEx(); + int x = e.getX(); + int y = e.getY(); + int clickCount = e.getClickCount(); + boolean popupTrigger = e.isPopupTrigger(); + int scrollType = e.getScrollType(); + int scrollAmount = e.getScrollAmount(); + int wheelRotation = e.getWheelRotation(); + return new MouseWheelEvent(source, id, when, modifiers, x, y, clickCount, popupTrigger, + scrollType, scrollAmount, wheelRotation); + } } 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 40621761b3..e8a117c8c4 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 @@ -27,6 +27,7 @@ import com.google.common.base.Function; import edu.uci.ics.jung.algorithms.layout.AbstractLayout; import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.graph.Graph; +import edu.uci.ics.jung.visualization.RenderContext; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel; import ghidra.graph.VisualGraph; @@ -126,7 +127,8 @@ public abstract class AbstractVisualGraphLayout getEdgeShapeTransformer() { + public Function getEdgeShapeTransformer(RenderContext context) { + edgeShapeTransformer.setRenderContext(context); return edgeShapeTransformer; } @@ -282,7 +284,7 @@ public abstract class AbstractVisualGraphLayout entry : entrySet) { V vertex = entry.getKey(); Point2D location = entry.getValue(); - setLocation(vertex, location); + setLocation(vertex, location, ChangeType.RESTORE); vertex.setLocation(location); } } @@ -698,10 +700,10 @@ public abstract class AbstractVisualGraphLayout> iterator = listeners.iterator(); for (; iterator.hasNext();) { LayoutListener layoutListener = iterator.next(); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java index be56cec4ef..eaf990726f 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java @@ -97,4 +97,14 @@ public class GridBounds { return true; } + public void transpose() { + int temp = minRow; + minRow = minCol; + minCol = temp; + + temp = maxRow; + maxRow = maxCol; + maxCol = temp; + } + } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java index 4ae8660a19..c9aa22dbd3 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java @@ -44,13 +44,13 @@ public class GridLocationMap { protected Map> edgePoints = new HashMap<>(); private GridBounds gridBounds = new GridBounds(); - // Tree based algorithms might want to track the column of the root node as it changes when - // the grid is shifted or merged.Useful for determining the position of a parent node when + // Tree based algorithms might want to track the location of the root node as it changes when + // the grid is shifted or merged. Useful for determining the position of a parent node when // building bottom up. - private int rootColumn = 0; + private GridPoint rootPoint; public GridLocationMap() { - rootColumn = 0; + rootPoint = new GridPoint(0, 0); } /** @@ -60,7 +60,7 @@ public class GridLocationMap { * @param col the column for the initial vertex. */ public GridLocationMap(V root, int row, int col) { - this.rootColumn = col; + rootPoint = new GridPoint(row, col); set(root, new GridPoint(row, col)); } @@ -69,7 +69,15 @@ public class GridLocationMap { * @return the column of the initial vertex in this grid */ public int getRootColumn() { - return rootColumn; + return rootPoint.col; + } + + /** + * Returns the row of the initial vertex in this grid. + * @return the row of the initial vertex in this grid + */ + public int getRootRow() { + return rootPoint.row; } public Set vertices() { @@ -245,7 +253,8 @@ public class GridLocationMap { p.row += rowShift; p.col += colShift; } - rootColumn += colShift; + rootPoint.row += rowShift; + rootPoint.col += colShift; gridBounds.shift(rowShift, colShift); } @@ -292,6 +301,30 @@ public class GridLocationMap { return rowRanges; } + /** + * Returns the minimum/max row for all columns in the grid. This method is only defined for + * grids that have no negative columns. This is because the array returned will be 0 based, with + * the entry at index 0 containing the row bounds for column 0 and so on. + * @return the minimum/max row for all columns in the grid + * @throws IllegalStateException if this method is called on a grid with negative rows. + */ + public GridRange[] getVertexRowRanges() { + if (gridBounds.minCol() < 0) { + throw new IllegalStateException( + "getVertexColumnRanges not defined for grids with negative rows!"); + } + GridRange[] colRanges = new GridRange[width()]; + + for (int i = 0; i < colRanges.length; i++) { + colRanges[i] = new GridRange(); + } + + for (GridPoint p : vertexPoints.values()) { + colRanges[p.col].add(p.row); + } + return colRanges; + } + public boolean containsVertex(V v) { return vertexPoints.containsKey(v); } @@ -371,7 +404,7 @@ public class GridLocationMap { private GridLocationMap copy() { GridLocationMap map = new GridLocationMap<>(); - map.rootColumn = rootColumn; + map.rootPoint = new GridPoint(rootPoint.row, rootPoint.col); Set> entries = vertexPoints.entrySet(); for (Entry entry : entries) { diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java index dfdb71b804..671d7cba4b 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java @@ -57,6 +57,12 @@ public class GridPoint { return col == other.col && row == other.row; } + public void transpose() { + int temp = row; + row = col; + col = temp; + } + @Override public String toString() { return "(r=" + row + ",c=" + col + ")"; diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/JungWrappingVisualGraphLayoutAdapter.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/JungWrappingVisualGraphLayoutAdapter.java index e7a12d891a..68f523e457 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/JungWrappingVisualGraphLayoutAdapter.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/JungWrappingVisualGraphLayoutAdapter.java @@ -4,9 +4,9 @@ * 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. @@ -26,6 +26,7 @@ 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.RenderContext; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel; import ghidra.graph.VisualGraph; @@ -182,7 +183,8 @@ public class JungWrappingVisualGraphLayoutAdapter getEdgeShapeTransformer() { + public Function getEdgeShapeTransformer(RenderContext context) { + edgeShapeTransformer.setRenderContext(context); return edgeShapeTransformer; } @@ -239,7 +241,7 @@ public class JungWrappingVisualGraphLayoutAdapter { public enum ChangeType { - USER, // real changes that should be tracked + API, // non-transient change to a vertex location made by an API call TRANSIENT, // transient changes that can be ignored - RESTORE // changes that happen when re-serializing saved locations + RESTORE, // changes that happen when re-serializing saved locations + USER; // user initiated change, such as the user dragging a vertex + + public boolean isTransitional() { + return this == RESTORE || this == TRANSIENT; + } } /** diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutPositions.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutPositions.java index ff6c3718b4..d2b7c9ef6c 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutPositions.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutPositions.java @@ -4,9 +4,9 @@ * 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. @@ -107,4 +107,9 @@ public class LayoutPositions> { vertexLocations.clear(); edgeArticulations.clear(); } + + @Override + public String toString() { + return vertexLocations.toString(); + } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/VisualGraphLayout.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/VisualGraphLayout.java index 108392a5b4..3f41cfbf9a 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/VisualGraphLayout.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/VisualGraphLayout.java @@ -4,9 +4,9 @@ * 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. @@ -21,6 +21,7 @@ import java.awt.geom.Point2D; import com.google.common.base.Function; import edu.uci.ics.jung.algorithms.layout.Layout; +import edu.uci.ics.jung.visualization.RenderContext; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import edu.uci.ics.jung.visualization.renderers.Renderer; import ghidra.graph.VisualGraph; @@ -91,7 +92,9 @@ public interface VisualGraphLayout cloneLayout(VisualGraph newGraph); /** - * Allows the client to change the location while specifying the type of change + * Allows the client to change the location while specifying the type of change. + *

+ * Calling {@link #setLocation(Object, Point2D)} will use {@link ChangeType#API}. * * @param v the vertex * @param location the new location @@ -116,9 +119,10 @@ public interface VisualGraphLayout getEdgeShapeTransformer(); + public Function getEdgeShapeTransformer(RenderContext context); /** * Returns an optional custom edge label renderer. This is used to add labels to the edges. diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/shape/ArticulatedEdgeTransformer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/shape/ArticulatedEdgeTransformer.java index 1fb83889cb..19c10e3a04 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/shape/ArticulatedEdgeTransformer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/shape/ArticulatedEdgeTransformer.java @@ -4,9 +4,9 @@ * 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. @@ -16,12 +16,15 @@ package ghidra.graph.viewer.shape; import java.awt.Shape; -import java.awt.geom.*; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; import java.util.List; import com.google.common.base.Function; +import edu.uci.ics.jung.visualization.RenderContext; import ghidra.graph.viewer.*; +import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; import ghidra.util.Msg; import ghidra.util.SystemUtilities; @@ -33,6 +36,12 @@ import ghidra.util.SystemUtilities; public class ArticulatedEdgeTransformer> implements Function { + private RenderContext renderContext; + + public void setRenderContext(RenderContext context) { + this.renderContext = context; + } + /** * Get the shape for this edge * @@ -45,6 +54,21 @@ public class ArticulatedEdgeTransformer vertexShapeTransformer = + renderContext.getVertexShapeTransformer(); + Shape vertexShape = getVertexShapeForEdge(end, vertexShapeTransformer); + Shape hollowEgdeLoop = GraphViewerUtils.createHollowEgdeLoop(); + + // we are not actually creating this in graph space, but by passing in 0,0, we are in + // unit space + return GraphViewerUtils.createEgdeLoopInGraphSpace(hollowEgdeLoop, vertexShape, 0, 0); + } + if (isLoop) { return GraphViewerUtils.createHollowEgdeLoop(); } @@ -82,43 +106,23 @@ public class ArticulatedEdgeTransformer Shape getVertexShapeForEdge(V v, Function vertexShaper) { + if (vertexShaper instanceof VisualGraphVertexShapeTransformer) { + if (v instanceof VisualVertex) { + VisualVertex vv = (VisualVertex) v; + + // Note: it is a bit odd that we 'know' to use the compact shape here for + // hit detection, but this is how the edge is painted, so we want the + // view to match the mouse. + return ((VisualGraphVertexShapeTransformer) vertexShaper).transformToCompactShape( + vv); + } } - - double theta = StrictMath.atan2(deltaY, deltaX); - transform.rotate(theta); - double scale = StrictMath.sqrt(deltaY * deltaY + deltaX * deltaX); - transform.scale(scale, 1.0f); - - // - // TODO - // The current design and use of this transformer is a bit odd. We currently have code - // to create the edge shape here and in the ArticulatedEdgeRenderer. Ideally, this - // class would be the only one that creates the edge shape. Then, any clients of the - // edge transformer would have to take the shape and then transform it to the desired - // space (the view or graph space). The transformations could be done using the - // GraphViewerUtils. - // - - try { - // TODO it is not clear why this is using an inverse transform; why not just create - // the transform that we want? - AffineTransform inverse = transform.createInverse(); - Shape transformedShape = inverse.createTransformedShape(path); - return transformedShape; - } - catch (NoninvertibleTransformException e1) { - Msg.error(this, "Unexpected exception transforming an edge", e1); - } - - return null; + return vertexShaper.apply(v); } private void logMissingLocation(E e, V v) { @@ -151,4 +155,5 @@ public class ArticulatedEdgeTransformer { + ReferenceManager refMgr = program.getReferenceManager(); + refMgr.addMemoryReference(addr(0x040011c), addr(0x0400000), RefType.DATA, + SourceType.ANALYSIS, 0); + }); + + env.showTool(); + } + + @Test + public void testDataGraph() { + go(0x4000e8); + performAction("Display Data Graph", "DataGraphPlugin", true); + + DataGraphProvider provider = getProvider(DataGraphProvider.class); + DegController controller = provider.getController(); + DataExplorationGraph graph = controller.getGraph(); + DataDegVertex root = (DataDegVertex) graph.getRoot(); + turnOffAnimation(controller); + + expandRow(root, 3); + openPointer(root, 13); + setSize(provider, 850, 550); + moveGraph(controller, -200, -280); + + waitForSwing(); + captureProvider(DataGraphProvider.class); + + } + + @Test + public void testCodeVertex() { + go(0x40b1f4); + performAction("Display Data Graph", "DataGraphPlugin", true); + + DataGraphProvider provider = getProvider(DataGraphProvider.class); + DegController controller = provider.getController(); + turnOffAnimation(controller); + + performAction("Incoming References", "DataGraphPlugin", provider, true); + + setSize(provider, 600, 400); + moveGraph(controller, -100, -350); + + waitForSwing(); + captureProvider(DataGraphProvider.class); + + } + + private void moveGraph(DegController controller, int deltaX, int deltaY) { + runSwing(() -> { + DegVertex root = controller.getGraph().getRoot(); + Point2D location = root.getLocation(); + controller.centerPoint( + new Point((int) location.getX() + deltaX, (int) location.getY() + deltaY)); + }); + } + + private void openPointer(DataDegVertex root, int rowIndex) { + runSwing(() -> { + root.openPointerReference(rowIndex); + }); + } + + private void expandRow(DataDegVertex root, int rowIndex) { + runSwing(() -> { + root.expand(rowIndex); + }); + } + + private void turnOffAnimation(DegController controller) { + runSwing(() -> { + GraphComponent comp = + controller.getView().getGraphComponent(); + VisualGraphOptions graphOptions = comp.getGraphOptions(); + graphOptions.setUseAnimation(false); + }); + } + + private void setSize(DataGraphProvider provider, int width, int height) { + runSwing(() -> { + Window window = tool.getWindowManager().getProviderWindow(provider); + if (window == null) { + throw new AssertException("Could not find window for " + + "provider--is it showing?: " + provider.getName()); + } + + window.setSize(new Dimension(width, height)); + }); + } +}