GP-5481 Created prototype data graph feature

This commit is contained in:
ghidragon 2025-07-02 13:20:47 -04:00
parent 8d95e97521
commit f54bd20d40
102 changed files with 9267 additions and 366 deletions

View file

@ -31,6 +31,7 @@ dependencies {
api project(':DecompilerDependent') api project(':DecompilerDependent')
api project(':FunctionGraph') api project(':FunctionGraph')
api project(':ProposedUtils') api project(':ProposedUtils')
api project(':DataGraph')
testImplementation project(path: ':Generic', configuration: 'testArtifacts') testImplementation project(path: ':Generic', configuration: 'testArtifacts')
testImplementation project(path: ':Base', configuration: 'testArtifacts') testImplementation project(path: ':Base', configuration: 'testArtifacts')

View file

@ -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);
}
}

View file

@ -73,6 +73,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
protected boolean useDebugBorders = false; protected boolean useDebugBorders = false;
private String fullLabelText; private String fullLabelText;
private int circleCenterYOffset;
public CircleWithLabelVertexShapeProvider(String label) { public CircleWithLabelVertexShapeProvider(String label) {
this.fullLabelText = label; this.fullLabelText = label;
@ -85,6 +86,10 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
buildUi(); buildUi();
} }
public int getCircleCenterYOffset() {
return circleCenterYOffset;
}
protected void buildUi() { protected void buildUi() {
String name = generateLabelText(); String name = generateLabelText();
@ -201,6 +206,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
vertexImageLabel.setBounds(x, y, size.width, size.height); vertexImageLabel.setBounds(x, y, size.width, size.height);
Dimension shapeSize = vertexShape.getBounds().getSize(); 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 // setFrame() will make sure the shape's x,y values are where they need to be
// for the later 'full shape' creation // for the later 'full shape' creation
@ -332,7 +338,7 @@ public class CircleWithLabelVertexShapeProvider implements VertexShapeProvider {
return false; return false;
} }
protected void setTogglesVisible(boolean visible) { public void setTogglesVisible(boolean visible) {
toggleInsButton.setVisible(visible); toggleInsButton.setVisible(visible);
toggleOutsButton.setVisible(visible); toggleOutsButton.setVisible(visible);
} }

View file

@ -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) { public void setRead(MemoryBlock block, boolean r) {
tx(() -> block.setRead(r)); tx(() -> block.setRead(r));
} }

View file

@ -0,0 +1 @@
EXCLUDE FROM GHIDRA JAR: true

View file

@ -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')
}

View file

@ -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|

View file

@ -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

View file

@ -0,0 +1,64 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
<tocref id="Graphing">
<tocdef id="Data Graph" text="Data Graph" target="help/topics/DataGraphPlugin/Data_Graph.html" >
<tocdef id="Vertices" sortgroup="a" text="Vertices" target="help/topics/DataGraphPlugin/Data_Graph.html#Vertices"/>
<tocdef id="Graph Actions" sortgroup="b" text="Graph Actions" target="help/topics/DataGraphPlugin/Data_Graph.html#Data_Graph_Actions" />
<tocdef id="Popups" sortgroup="c" text="Popups" target="help/topics/DataGraphPlugin/Data_Graph.html#Popups" />
<tocdef id="Layout" sortgroup="d" text="Layout" target="help/topics/DataGraphPlugin/Data_Graph.html#Layout" />
</tocdef>
</tocref>
</tocroot>

View file

@ -0,0 +1,250 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Function Graph Plugin</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Data_Graph"></A> <A name="DataGraphPlugin"></A>Data Graph</H1>
<TABLE x-use-null-cells="" width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" border="1" src="images/DataGraph.png"></TD>
</TR>
</TBODY>
</TABLE>
<BLOCKQUOTE>
<P>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.</P>
<P>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 <IMG src="Icons.RIGHT_ICON" alt="" border="0"> icon that can be clicked
to quickly explore that reference.</P>
<P>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.</P>
<P>The display consists of the <A href="#Primary_View">Primary View</A> and an optional <A
href="help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A>.</P>
</BLOCKQUOTE>
<H2><A name="Primary_View"></A>Primary View</H2>
<BLOCKQUOTE>
<P>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.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">The original source data vertex
has a <IMG src="Icons.HOME_ICON" alt="" border="0"> icon in its header to indicate it is the original source vertex.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Vertices"></A>Vertices</H2>
<BLOCKQUOTE>
<H3>Data Vertices</H3>
<BLOCKQUOTE>
<P>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.</P>
<P>Any elements that are pointers (or have attached outgoing references) will display a
<IMG src="Icons.RIGHT_ICON" alt="" border="0"> icon that can be clicked to explore those
references to add new vertices.</P>
<P>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.</P>
<P>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.</P>
<P>As long as you are within the <A href="help/topics/VisualGraph/Visual_Graph.html#Interaction_Threshold">interaction threshold</A>,
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.</P>
</BLOCKQUOTE>
<H3>Code Vertices</H3>
<BLOCKQUOTE>
<P>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.</P>
<TABLE x-use-null-cells="" width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" border="1" src=
"images/CodeVertex.png"></TD>
</TR>
</TBODY>
</TABLE>
<P>In the image above, the graph is showing two references to the same string.</P>
</BLOCKQUOTE>
<H3><A name="Layout">Vertex Layout</A></H3>
<BLOCKQUOTE>
<P>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.</P>
<P>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.</P>
<P>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.</P>
<P>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.</P>
<P>Whenever a vertex is removed from the graph, all vertices that were discovered by exploring
from that vertex are also removed.</P>
</BLOCKQUOTE>
<H3>Vertex Actions</H3>
<BLOCKQUOTE>
<P>The following toolbar actions are available on a data vertex.</P>
<UL>
<LI><A name="Expand_All"><IMG alt="" src="Icons.EXPAND_ALL_ICON">&nbsp<B> Expand All</B> -
Expands all expandable sub-data elements recursively contained in the data object. Note
that this action is not available if the data has no sub-data elements.</A></LI>
<LI><A name="Collapse_All"><IMG alt="" src="Icons.COLLAPSE_ALL_ICON">&nbsp<B>Collapse All</B>
- Collapses all expanded sub-data elements. Note that this action is not available if the
data has no sub-data elements.</A></LI>
<LI><A name="Delete_Vertex"><IMG alt="" src="Icons.CLOSE_ICON">&nbsp<B>Delete Vertex</B> - Removes this vertex and all vertices
that descend from this vertex.</A></LI>
</UL>
<P><A name="Popups">The following popup actions are are available depending on where the
mouse is positioned when the popup is triggered.</A></P>
<UL>
<LI><A name="Add_Outgoing"><B>Add All Outgoing References</B> - All outgoing references
from this data element or its sub-elements will generate a new vertex in the graph, if
not already present.</A></LI>
<LI><A name="Add_Incoming"><B>Add All Incoming References</B> - The program will be
searched for any references to this data element or its sub-elents and a new vertex be
created each discovered reference, if not already present.</A></LI>
<LI><A name="Expand_Fully"><B>Expand Fully</B> - If the mouse is over an
expandable row in data vertex, the vertex and all it's child elements will be fully
expanded.</A></LI>
<LI><A name="Original_Source"><B>Set Original Source</B> - Makes this the original source
root vertex. All other vertices are reorganized and laid out as if they were discovered
by following references from this vertex.See</A> <A href="#Layout">Vertex
Layout.</A></LI>
<LI><A name="Delete_Selected"><B>Delete Selected Vertices</B> - Deletes the selected
vertices and any descendants vertices (vertices that were discovered via exploring from
that vertex.)</A></LI>
</UL>
</BLOCKQUOTE>
<H3>Selecting Vertices</H3>
<BLOCKQUOTE>
<P>Left-clicking a vertex will select that vertex. To select multiple vertices, hold down
the <B><TT>Ctrl</TT></B> key while clicking. To deselect
a block, hold the <TT>Ctrl</TT> 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.</P>
<P>You may also select multiple vertices in one action by holding the <B><TT>Ctrl</TT></B> key
while performing a drag operation. Press the <B><TT>Ctrl</TT></B> 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.</P>
</BLOCKQUOTE>
<H3>Navigating Vertices</H3>
<BLOCKQUOTE>
<P>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.</P>
<P>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.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Data_Graph_Actions"></A>Data Graph Actions</H2>
<BLOCKQUOTE>
<H3>Toolbar Actions</H3>
<UL>
<LI><A name="Relayout_Graph"><IMG alt="" src=
"icon.plugin.datagraph.action.viewer.reset">&nbsp<B>Refresh Layout</B> - All manually positioned
vertices will be reset and the graph will relayout to its automated locations.</A></LI>
<LI><A name="Select_Home_Vertex"><IMG alt="" src=
"Icons.HOME_ICON">&nbsp<B>Go To Source Vertex</B> - The original source vertex will be selected
and centered in the graph.</A></LI>
<LI><A name="Navigate_In"><IMG alt="" src=
"Icons.NAVIGATE_ON_INCOMING_EVENT_ICON">&nbsp<B>Navigate In</B> - If selected, the graph will
listen for tool location events and select the vertex that contains the location address,
is one exists.</A></LI>
<LI><A name="Navigate_Out"><IMG alt="" src=
"Icons.NAVIGATE_ON_OUTGOING_EVENT_ICON">&nbsp<B>Navigate Out</B> - If selected, the graph will
generate tool location events when vertices are selected or rows within a vertex are
selected.</A></LI>
<LI><A name="Expanded_Format"><IMG alt="" src=
"icon.plugin.datagraph.action.viewer.vertex.format">&nbsp<B>Expanded Format</B> - If selected,
vertices will show more information for each row in the display. In compact mode, a data
row will generally show the field name and its value. In expanded mode, a data row will
generally show the datatype, field name, and its value.</A></LI>
</UL>
</BLOCKQUOTE
<H2>Standard Graph Features and Actions</H2>
<BLOCKQUOTE>
<P>The data graph is a type of Ghidra Visual Graph and has some standard concepts, features
and actions.
<BLOCKQUOTE>
<UL>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Pan">Panning</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Zoom">Zooming</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Interaction_Threshold">Interaction Threshold</A></LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Data Graph Plugin</I></P>
<BR>
<BR>
</BODY>
</HTML>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -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 datagraph;
import java.util.HashSet;
import java.util.Set;
import docking.action.builder.ActionBuilder;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Data;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
/**
* Base class for plugins that show a graph of data from program.
*/
public abstract class AbstractDataGraphPlugin extends ProgramPlugin {
private Set<DataGraphProvider> 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);
}

View file

@ -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);
}
}

View file

@ -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<DegVertex, DegEdge, DataExplorationGraph> {
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<DegVertex, DegEdge, DataExplorationGraph> getView() {
return controller.getView();
}
@Override
public ActionContext getActionContext(MouseEvent event) {
Set<DegVertex> 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<DegVertex, DegEdge> viewer = (GraphViewer<DegVertex, DegEdge>) source;
VertexMouseInfo<DegVertex, DegEdge> 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<DegVertex> 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);
}
}

View file

@ -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<DegVertex> {
private Set<DegVertex> selectedVertices;
public DegContext(DataGraphProvider dataGraphProvider, DegVertex targetVertex,
Set<DegVertex> selectedVertices) {
this(dataGraphProvider, targetVertex, selectedVertices, null);
}
public DegContext(DataGraphProvider dataGraphProvider,
DegVertex targetVertex, Set<DegVertex> selectedVertices,
VertexMouseInfo<DegVertex, DegEdge> vertexMouseInfo) {
super(dataGraphProvider, targetVertex);
this.selectedVertices = selectedVertices;
}
public Set<DegVertex> getSelectedVertices() {
return selectedVertices;
}
@Override
public boolean shouldShowSatelliteActions() {
return getVertex() == null;
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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<DataRowObject> 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;
}
}

View file

@ -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<DegVertex, DegEdge> {
private VisualGraphLayout<DegVertex, DegEdge> 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<DegVertex, DegEdge> 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<DegVertex, DegEdge> layout) {
this.layout = layout;
}
}

View file

@ -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<DegVertex, DegEdge> 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<DegVertex, DegEdge> viewUpdater = view.getViewUpdater();
viewUpdater.moveVertexToCenterWithAnimation(vertex);
}
public void centerPoint(Point point) {
VisualGraphViewUpdater<DegVertex, DegEdge> 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<DegVertex> selectedVertices) {
Set<DegVertex> 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<DegVertex> 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<Reference> refs = XReferenceUtils.getXReferences(target, MAX_REFS + 1);
if (refs.size() < MAX_REFS) {
int offcutMax = MAX_REFS + 1 - refs.size();
List<Reference> 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<DegVertex> 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<DegVertex, DegEdge, DataExplorationGraph> 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<DegVertex> getOutgoingVertices(DataDegVertex vertex) {
Collection<DegEdge> 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<DegVertex> getIncomingVertices(DataDegVertex vertex) {
Collection<DegEdge> inEdges = graph.getOutEdges(vertex);
return inEdges.stream().map(e -> e.getStart()).collect(Collectors.toSet());
}
public DataExplorationGraph getGraph() {
return graph;
}
public GraphViewer<DegVertex, DegEdge> 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<DegVertex> vertices = graph.getVertices();
for (DegVertex dgVertex : vertices) {
if (dgVertex.containsAddress(address)) {
navigateTo(dgVertex);
break;
}
}
}
private void doAddIncomingReferences(DataDegVertex sourceVertex, Listing listing,
List<Reference> 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<Address> getOutgoingReferenceAddresses(Data data) {
Set<Address> 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<DegVertex, DegEdge> viewUpdater = view.getViewUpdater();
GraphViewer<DegVertex, DegEdge> viewer = view.getPrimaryGraphViewer();
RelayoutAndEnsureVisible<DegVertex, DegEdge> job =
new RelayoutAndEnsureVisible<>(viewer, primary, secondary,
viewUpdater.isAnimationEnabled());
viewUpdater.scheduleViewChangeJob(job);
}
void relayoutGraphAndCenter(DegVertex vertex) {
VisualGraphViewUpdater<DegVertex, DegEdge> viewUpdater = view.getViewUpdater();
GraphViewer<DegVertex, DegEdge> viewer = view.getPrimaryGraphViewer();
RelayoutAndCenterVertexGraphJob<DegVertex, DegEdge> 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<DegVertex> 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<DegVertex> added) {
Set<Address> 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<DegVertex> 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<DegVertex> toDelete, DegVertex dgVertex) {
Collection<DegEdge> 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<DegVertex, DegEdge> graphMouse =
view.getPrimaryGraphViewer().getGraphMouse();
graphMouse.prepend(new DataMousePlugin());
}
private VisualGraphLayout<DegVertex, DegEdge> 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<DegVertex, DegEdge> 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<DegVertex, DegEdge> dragStart;
private Dimension startSize;
private boolean isFiringWheelEvent;
public DataMousePlugin() {
super(0);
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (isFiringWheelEvent) {
return;
}
VertexMouseInfo<DegVertex, DegEdge> 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<DegVertex, DegEdge> getTranslatedMouseInfo(MouseEvent e) {
GraphViewer<DegVertex, DegEdge> 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<DegVertex, DegEdge> 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<DegVertex, DegEdge> 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
}
}
}

View file

@ -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<DegVertex> {
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);
}
}

View file

@ -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<DegVertex, DegEdge, DataExplorationGraph> {
DegGraphView() {
super();
setSatelliteVisible(false);
}
@Override
protected void installGraphViewer() {
super.installGraphViewer();
GraphViewer<DegVertex, DegEdge> viewer = graphComponent.getPrimaryViewer();
viewer.setVertexTooltipProvider(new DataGraphVertexTipProvider());
}
private class DataGraphVertexTipProvider implements VertexTooltipProvider<DegVertex, DegEdge> {
@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);
}
}
}

View file

@ -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<DegVertex, DegEdge> {
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<DegVertex, DegEdge> createClonedLayout(
VisualGraph<DegVertex, DegEdge> 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<DegVertex> getIncommingVertexComparator() {
return (v1, v2) -> v1.getAddress().compareTo(v2.getAddress());
}
@Override
protected Comparator<DegVertex> getOutgoingVertexComparator() {
return (v1, v2) -> {
DegVertex parent = (DegVertex) v1.getSourceVertex();
return parent.compare(v1, v2);
};
}
@Override
protected EgEdgeTransformer<DegVertex, DegEdge> createEdgeTransformer() {
return new EgEdgeTransformer<>();
}
}

View file

@ -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<DegVertex, DegEdge, DataExplorationGraph> {
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<DegVertex, DegEdge> 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;
}
}

View file

@ -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<DegVertex> {
// 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);
}

View file

@ -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<DataRowObject> gTrable;
private DataTrableRowModel model;
private JScrollPane scroll;
private int headerHeight;
private Comparator<int[]> pathComparator = new DtComponentPathComparator();
private Dimension userSize;
private Rectangle preferredShape = new Rectangle(0, 0, 0, 0);
private DataDegVertex vertex;
private DegController controller;
private Map<EgVertex, IncomingEdgeOffsetInfo> incomingEdgeOffsetMap = new HashMap<>();
private Map<EgVertex, OutgoingEdgeOffsetInfo> outgoingEdgeOffsetMap = new HashMap<>();
private boolean cachedOutgoingOffsetsValid;
private boolean cachedIncomingOffsetsValid;
private GTrableColumnModel<DataRowObject> 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<DataRowObject> getRowObjects() {
List<DataRowObject> 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<DegVertex> outgoingVertices = controller.getOutgoingVertices(vertex);
outgoingEdgeOffsetMap.keySet().retainAll(outgoingVertices);
Set<DegVertex> 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<Address> 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<int[]> 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<int[]> paths, int[] componentPath) {
int index = Collections.binarySearch(paths, componentPath, pathComparator);
if (index < 0) {
index = -index - 2;
}
return index;
}
private int getIndex(List<Address> 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<Address> getVisibleAddresses() {
Range visibleRows = gTrable.getVisibleRows();
List<Address> 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<int[]> getVisibleDataPaths() {
Range visibleRows = gTrable.getVisibleRows();
List<int[]> 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;
}
}
}

View file

@ -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<int[]> {
@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;
}
}

View file

@ -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<DataRowObject> {
@Override
protected void populateColumns(List<GTrableColumn<DataRowObject, ?>> 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<DataRowObject, String> {
@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<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getDataType();
}
@Override
protected int getPreferredWidth() {
return 120;
}
}
private class ValueColumn extends GTrableColumn<DataRowObject, String> {
private GTrableCellRenderer<String> 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<String> getRenderer() {
return renderer;
}
}
private static class PointerButtonColumn extends GTrableColumn<DataRowObject, Boolean> {
private GTrableCellRenderer<Boolean> 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<Boolean> getRenderer() {
return renderer;
}
}
}

View file

@ -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<DataRowObject> {
@Override
protected void populateColumns(List<GTrableColumn<DataRowObject, ?>> 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<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getName();
}
@Override
protected int getPreferredWidth() {
return 150;
}
}
private static class TypeColumn extends GTrableColumn<DataRowObject, String> {
@Override
public String getValue(DataRowObject row) {
return row.getDataType();
}
@Override
protected int getPreferredWidth() {
return 120;
}
}
private static class ValueColumn extends GTrableColumn<DataRowObject, String> {
private GTrableCellRenderer<String> 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<String> getRenderer() {
return renderer;
}
}
private static class PointerButtonColumn extends GTrableColumn<DataRowObject, Boolean> {
private GTrableCellRenderer<Boolean> 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<Boolean> getRenderer() {
return renderer;
}
}
}

View file

@ -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<Boolean> {
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;
}
}

View file

@ -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<String> {
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;
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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();
}

View file

@ -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<DataRowObject> 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();
}
}

View file

@ -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<DataRowObject> {
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();
}
}

View file

@ -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.
* <P>
* 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<OpenDataChildren> {
// 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<OpenDataChildren> 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<OpenDataChildren> 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;
}
}
}

View file

@ -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 <V> the vertex type
* @param <E> the edge type
*/
public abstract class AbstractExplorationGraph<V extends EgVertex, E extends EgEdge<V>>
extends DefaultVisualGraph<V, E> {
private V root;
private EgGraphLayout<V, E> 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<V, E> layout) {
this.layout = layout;
}
@Override
public VisualGraphLayout<V, E> 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<V> getDescendants(V source) {
Set<V> 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<V> 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<V> 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<V> vertexQueue) {
while (!vertexQueue.isEmpty()) {
V remove = vertexQueue.remove();
processEdges(remove, vertexQueue);
}
}
private void processEdges(V v, Queue<V> vertexQueue) {
Collection<E> outEdges = getOutEdges(v);
for (EgEdge<V> edge : outEdges) {
V next = edge.getEnd();
if (next.getSourceVertex() == null) {
next.setSource(v);
vertexQueue.add(next);
}
}
Collection<E> inEdges = getInEdges(v);
for (E e : inEdges) {
V previous = e.getStart();
if (previous.getSourceVertex() == null) {
previous.setSource(v);
vertexQueue.add(previous);
}
}
}
}

View file

@ -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 <V> The vertex type
*/
public abstract class EgEdge<V extends EgVertex> extends AbstractVisualEdge<V> {
public EgEdge(V start, V end) {
super(start, end);
}
}

View file

@ -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 <V> the vertex type
* @param <E> the edge type
*/
public class EgEdgeRenderer<V extends EgVertex, E extends VisualEdge<V>>
extends VisualEdgeRenderer<V, E> {
@Override
public Shape getEdgeShape(RenderContext<V, E> rc, Graph<V, E> graph, E e, float x1, float y1,
float x2, float y2, boolean isLoop, Shape vertexShape) {
Function<? super E, Shape> 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);
}
}

View file

@ -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 <V> the vertex type
* @param <E> the edge type
*/
public class EgEdgeTransformer<V extends EgVertex, E extends VisualEdge<V>>
implements Function<E, Shape> {
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;
}
}

View file

@ -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.
* <P>
* 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 <V> the vertex type
* @param <E> the edge type
*/
public abstract class EgGraphLayout<V extends EgVertex, E extends EgEdge<V>>
extends AbstractVisualGraphLayout<V, E> {
private EgEdgeRenderer<V, E> edgeRenderer = new EgEdgeRenderer<>();
private EgEdgeTransformer<V, E> edgeTransformer;
private Function<V, Shape> vertexShapeTransformer = new VisualGraphVertexShapeTransformer<V>();
protected int verticalGap;
protected int horizontalGap;
protected EgGraphLayout(AbstractExplorationGraph<V, E> graph, String name, int verticalGap,
int horizontalGap) {
super(graph, name);
this.verticalGap = verticalGap;
this.horizontalGap = horizontalGap;
this.edgeTransformer = createEdgeTransformer();
}
protected abstract EgEdgeTransformer<V, E> createEdgeTransformer();
protected abstract Comparator<V> getIncommingVertexComparator();
protected abstract Comparator<V> getOutgoingVertexComparator();
@Override
public Point2D apply(V v) {
if (v.hasUserChangedLocation()) {
return v.getLocation();
}
return super.apply(v);
}
@SuppressWarnings("unchecked")
@Override
public VisualGraph<V, E> getVisualGraph() {
return (VisualGraph<V, E>) getGraph();
}
@Override
public BasicEdgeRenderer<V, E> getEdgeRenderer() {
return edgeRenderer;
}
@Override
public com.google.common.base.Function<E, Shape> getEdgeShapeTransformer(
RenderContext<V, E> context) {
return edgeTransformer;
}
@Override
protected LayoutPositions<V, E> doCalculateLocations(VisualGraph<V, E> g,
TaskMonitor taskMonitor) {
if (!(g instanceof AbstractExplorationGraph<V, E> layeredGraph)) {
throw new IllegalArgumentException("This layout only supports Layered graphs!");
}
try {
monitor = taskMonitor;
return computePositions(layeredGraph);
}
finally {
monitor = TaskMonitor.DUMMY;
}
}
private LayoutPositions<V, E> computePositions(AbstractExplorationGraph<V, E> g) {
GraphLocationMap<V> locationMap = getLocationMap(g, g.getRoot());
Map<V, Point2D> vertexLocations = locationMap.getVertexLocations();
return LayoutPositions.createNewPositions(vertexLocations,
Collections.emptyMap());
}
@Override
protected GridLocationMap<V, E> performInitialGridLayout(VisualGraph<V, E> g) {
// we override the method that calls this abstract method, so it isn't used.
throw new UnsupportedOperationException();
}
private GraphLocationMap<V> getLocationMap(AbstractExplorationGraph<V, E> g, V v) {
List<GraphLocationMap<V>> leftMaps = getMapsForIncommingEdges(g, v);
List<GraphLocationMap<V>> rightMaps = getMapsForOutgoingEdges(g, v);
Shape shape = vertexShapeTransformer.apply(v);
Rectangle bounds = shape.getBounds();
GraphLocationMap<V> 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<V> baseMap, List<GraphLocationMap<V>> leftMaps) {
int shiftY = getTopGroupShift(leftMaps, verticalGap);
int baseShiftX = baseMap.getMinX() - horizontalGap;
for (GraphLocationMap<V> 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<V> baseMap, List<GraphLocationMap<V>> rightMaps) {
int shiftY = getTopGroupShift(rightMaps, verticalGap);
int baseShiftX = baseMap.getMaxX() + horizontalGap;
for (GraphLocationMap<V> 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<GraphLocationMap<V>> maps, int gap) {
int totalHeight = 0;
for (GraphLocationMap<V> map : maps) {
totalHeight += map.getHeight();
}
totalHeight += (maps.size() - 1) * gap;
return -totalHeight / 2;
}
private List<GraphLocationMap<V>> getMapsForOutgoingEdges(AbstractExplorationGraph<V, E> g,
V v) {
List<E> edges = getOutgoingNextLayerEdges(g, v);
if (edges == null || edges.isEmpty()) {
return null;
}
return getOutgoingGraphMaps(g, edges);
}
private List<GraphLocationMap<V>> getMapsForIncommingEdges(AbstractExplorationGraph<V, E> g,
V v) {
List<E> edges = getIncommingNextLayerEdges(g, v);
if (edges == null || edges.isEmpty()) {
return null;
}
return getIncomingGraphMaps(g, edges);
}
private List<GraphLocationMap<V>> getOutgoingGraphMaps(AbstractExplorationGraph<V, E> g,
List<E> edges) {
List<GraphLocationMap<V>> maps = new ArrayList<>(edges.size());
for (E e : edges) {
maps.add(getLocationMap(g, e.getEnd()));
}
return maps;
}
private List<GraphLocationMap<V>> getIncomingGraphMaps(AbstractExplorationGraph<V, E> g,
List<E> edges) {
List<GraphLocationMap<V>> maps = new ArrayList<>(edges.size());
for (E e : edges) {
maps.add(getLocationMap(g, e.getStart()));
}
return maps;
}
private List<E> getOutgoingNextLayerEdges(AbstractExplorationGraph<V, E> g, V v) {
Collection<E> outEdges = g.getOutEdges(v);
if (outEdges == null || outEdges.isEmpty()) {
return null;
}
List<E> nextLayerEdges = new ArrayList<>();
for (E e : outEdges) {
if (v.equals(e.getEnd().getSourceVertex())) {
nextLayerEdges.add(e);
}
}
Comparator<V> c = getOutgoingVertexComparator();
nextLayerEdges.sort((e1, e2) -> c.compare(e1.getEnd(), e2.getEnd()));
return nextLayerEdges;
}
private List<E> getIncommingNextLayerEdges(AbstractExplorationGraph<V, E> g, V v) {
Collection<E> inEdges = g.getInEdges(v);
if (inEdges == null || inEdges.isEmpty()) {
return null;
}
List<E> nextLayerEdges = new ArrayList<>();
for (E e : inEdges) {
if (v.equals(e.getStart().getSourceVertex())) {
nextLayerEdges.add(e);
}
}
Comparator<V> 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);
}
}

View file

@ -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;
}
}

View file

@ -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 <V> the vertex type
*/
public class GraphLocationMap<V> {
private Map<V, Point> 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<V> 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<Point> 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<V, Point2D> getVertexLocations() {
Map<V, Point2D> 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

View file

@ -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<DegVertex> 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<DegVertex> 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<DegVertex> 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<DataRowObject> rowObjects = ((DataDegVertex) vertex).getRowObjects();
assertEquals(expectedRowCount, rowObjects.size());
}
private DegVertex getFocusedVertex() {
Collection<DegVertex> 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<DegVertex> getNonRootVertices() {
Set<DegVertex> 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<DegVertex> 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<String> actualRows = getRowsAsText((DataDegVertex) v);
int row = actualRows.indexOf(text);
assertTrue(row >= 0);
return row;
}
private void assertField(DegVertex v, String... expectedRows) {
List<String> actualRows = getRowsAsText((DataDegVertex) v);
assertEquals(expectedRows.length, actualRows.size());
List<String> expectedList = Arrays.asList(expectedRows);
assertListEqualOrdered(expectedList, actualRows);
}
private List<String> getRowsAsText(DataDegVertex v) {
List<DataRowObject> rows = runSwing(() -> v.getRowObjects());
//@formatter:off
List<String> 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<DegVertex, DegEdge> 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<DegVertex, DegEdge> getGraphUpdater() {
GraphViewer<DegVertex, DegEdge> viewer = controller.getPrimaryViewer();
VisualGraphViewUpdater<DegVertex, DegEdge> updater = viewer.getViewUpdater();
assertNotNull(updater);
return updater;
}
private void selectVertices(Set<DegVertex> newVertices) {
GraphViewer<DegVertex, DegEdge> viewer = controller.getPrimaryViewer();
GPickedState<DegVertex> pickState = viewer.getGPickedVertexState();
runSwing(() -> {
pickState.clear();
for (DegVertex dgVertex : newVertices) {
pickState.pick(dgVertex, true);
}
});
waitForSwing();
}
private void turnOffAnimation() {
runSwing(() -> {
GraphComponent<DegVertex, DegEdge, DataExplorationGraph> comp =
controller.getView().getGraphComponent();
VisualGraphOptions graphOptions = comp.getGraphOptions();
graphOptions.setUseAnimation(false);
});
}
}

View file

@ -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<TestVertex, TestEdge, TestExplorationGraph> 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<TestVertex> {
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<TestVertex, TestEdge> {
TestExplorationGraph(TestVertex root) {
super(root);
}
@Override
public DefaultVisualGraph<TestVertex, TestEdge> copy() {
Collection<TestVertex> v = getVertices();
Collection<TestEdge> e = getEdges();
TestExplorationGraph newGraph = new TestExplorationGraph(getRoot());
v.forEach(newGraph::addVertex);
e.forEach(newGraph::addEdge);
return newGraph;
}
}
private static class TestEgGraphLayout
extends EgGraphLayout<TestVertex, TestEdge> {
protected TestEgGraphLayout(TestExplorationGraph graph, TestVertex root) {
super(graph, "Test", VERTEX_GAP, VERTEX_GAP);
}
@Override
public AbstractVisualGraphLayout<TestVertex, TestEdge> createClonedLayout(
VisualGraph<TestVertex, TestEdge> newGraph) {
throw new UnsupportedOperationException();
}
@Override
protected EgEdgeTransformer<TestVertex, TestEdge> createEdgeTransformer() {
return new EgEdgeTransformer<EgGraphLayoutTest.TestVertex, EgGraphLayoutTest.TestEdge>();
}
@Override
protected Comparator<TestVertex> getIncommingVertexComparator() {
return (v1, v2) -> v1.name.compareTo(v2.name);
}
@Override
protected Comparator<TestVertex> getOutgoingVertexComparator() {
return (v1, v2) -> v1.name.compareTo(v2.name);
}
}
}

View file

@ -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<TestVertex> map1;
private GraphLocationMap<TestVertex> 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;
}
}
}

View file

@ -54,7 +54,7 @@
<tocdef id="Function Graph" text="Function Graph" target="help/topics/FunctionGraphPlugin/Function_Graph.html" > <tocdef id="Function Graph" text="Function Graph" target="help/topics/FunctionGraphPlugin/Function_Graph.html" >
<tocdef id="Primary View" sortgroup="a" text="Primary View" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Primary_View" /> <tocdef id="Primary View" sortgroup="a" text="Primary View" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Primary_View" />
<tocdef id="Satellite View" sortgroup="b" text="Satellite View" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Satellite_View" /> <tocdef id="Satellite View" sortgroup="b" text="Satellite View" target="help/topics/VisualGraph/Visual_Graph.html#Satellite_View" />
<tocdef id="Vertices" sortgroup="c" text="Vertices" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertices"> <tocdef id="Vertices" sortgroup="c" text="Vertices" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertices">
<tocdef id="Vertex Grouping" sortgroup="a" text="Vertex Grouping" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertex_Grouping" /> <tocdef id="Vertex Grouping" sortgroup="a" text="Vertex Grouping" target="help/topics/FunctionGraphPlugin/Function_Graph.html#Vertex_Grouping" />

View file

@ -29,7 +29,7 @@
"help/topics/CodeBrowserPlugin/CodeBrowser.htm">Listing</A>.</P> "help/topics/CodeBrowserPlugin/CodeBrowser.htm">Listing</A>.</P>
<P>The display consists of the <A href="#Primary_View">Primary View</A> and the <A href= <P>The display consists of the <A href="#Primary_View">Primary View</A> and the <A href=
"#Satellite_View">Satellite View</A>. There is also a group of <A href= "help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A>. There is also a group of <A href=
"#Function_Graph_Actions">actions</A> that apply to the entire graph.</P> "#Function_Graph_Actions">actions</A> that apply to the entire graph.</P>
</BLOCKQUOTE> </BLOCKQUOTE>
@ -94,60 +94,6 @@
Automatic Graph Relayout</B></P> Automatic Graph Relayout</B></P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H2><A name="Satellite_View"></A>Satellite View</H2>
<BLOCKQUOTE>
<P>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 <B>lens</B> (the white rectangle) that indicates how much of the
current graph fits into the primary view.</P>
<P>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.</P>
<P>You may hide the satellite view by right-clicking anywhere in the Primary View and
deselecting the <B>Display Satellite View</B> toggle button from the popup menu.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> If the Primary View is painting
sluggishly, then hiding the Satellite View cause the Primary View to be more
responsive.</P>
</BLOCKQUOTE>
<H3><A name="Satellite_View_Dock"></A>Detached Satellite</H3>
<BLOCKQUOTE>
<P>The Satellite View is attached, or <B>docked</B>, 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.</P>
<P>To undock the Satellite View, right-click in the graph and deselect the <B>Dock
Satellite View</B> menu item.</P>
<P>To re-dock the Satellite View, right-click in the graph and select the <B>Dock Satellite
View</B> menu item.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> To reshow the Satellite View if it is
hidden, whether docked or undocked, you can press the <IMG src=
"images/network-wireless.png" alt="" border="1"> 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.</P>
</BLOCKQUOTE>
<H3><A name="Satellite_Location"></A>Docked Satellite Location</H3>
<BLOCKQUOTE>
<P>When the Satellite View is attached, or <B>docked</B>, to the Primary View, you
can choose which corner to show the satellite view. To change the
corner, right-click in the graph, select <B>Docked Satellite Position</B> and then
select the appropriate sub-menu for the desired corner.<P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Vertices"></A>Vertices (Blocks)</H2> <H2><A name="Vertices"></A>Vertices (Blocks)</H2>
<BLOCKQUOTE> <BLOCKQUOTE>
@ -896,54 +842,16 @@
<BR> <BR>
</BLOCKQUOTE> </BLOCKQUOTE>
<H2><A name="Pan"></A>Panning</H2>
<BLOCKQUOTE>
<P>There are various ways to move the graph. To move the graph in any direction you can drag
from the whitespace of the graph.</P>
<P>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
<B>Scroll Wheel Pans</B> option. When this option is on, you can zoom by holding the
<TT><B>Control</B></TT> key (<TT><B>Command</B></TT> key on the Mac) while using the scroll
wheel. Alternatively, you can move the graph left to right using the mouse while
holding <TT><B>Control-Alt</B></TT>.</P>
<P>The satellite viewer may also be used to move the primary graphs view by dragging and
clicking inside of the satellite viewer.</P>
</BLOCKQUOTE>
<H2><A name="Zoom"></A>Zooming</H2> <H2><A name="Zoom"></A>Zooming</H2>
<BLOCKQUOTE> <BLOCKQUOTE>
<P>At <B>full zoom</B>, or <B>block level zoom</B>, each block is rendered at its natural <P>At <B>full zoom</B>, or <B>block level zoom</B>, each block is rendered at its natural
size, which is the same scale as Ghidra's primary <A href= size, which is the same scale as Ghidra's primary <A href=
"help/topics/CodeBrowserPlugin/CodeBrowser.htm">Listing</A>. From that point, which is a 1:1 "help/topics/CodeBrowserPlugin/CodeBrowser.htm">Listing</A>. 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.</P> zoom level, you can zoom out in order to fit more of the graph into the display. See
<A href="help/topics/VisualGraph/Visual_Graph.html#Zoom">Visual Graph Zooming</A> for
<BLOCKQUOTE> more information on graph zooming.</P>
<P>To change the zoom you may use the mouse scroll wheel while holding the
<TT><B>Control</B></TT> key (<TT><B>Command</B></TT> 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 <B>Zoom to Vertex</B> and <B>Zoom to Window</B>, respectively.</P>
<P><IMG src="help/shared/note.png" alt="Note" border="0"> To have the scroll wheel zoom
without holding the <TT><B>Control</B></TT> key, you can disable the <B>Scroll Wheel
Pans</B> option.</P>
<P><IMG src="help/shared/note.png" alt="Note" border="0"> To zoom the graph incrementally
using the keyboard you can use the <B>Zoom In</B> and <B>Zoom Out</B> actions. These
actions have default keybindings of <TT><B>Control-Minus</B></TT> and
<TT><B>Control-Equals</B></TT>.</P>
</BLOCKQUOTE>
<P>The <A href="#Satellite_View">satellite viewer</A> is always zoomed out far enough to fit
the entire graph into its window.</P>
</BLOCKQUOTE>
<H3>Vertex Quick Zoom</H3> <H3>Vertex Quick Zoom</H3>
<P>If you double-click a block header, then the <A href="#Zoom">Zoom Level</A> of the <P>If you double-click a block header, then the <A href="#Zoom">Zoom Level</A> of the
@ -954,48 +862,6 @@
</BLOCKQUOTE> </BLOCKQUOTE>
<H3><A name="Interaction_Threshold"></A>Interaction Threshold</H3> <H3><A name="Interaction_Threshold"></A>Interaction Threshold</H3>
<BLOCKQUOTE>
<P>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.</P>
<TABLE x-use-null-cells="" width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG border="1" src=
"images/FunctionGraph_Vertex_Drop_Shadow.png"></TD>
</TR>
</TBODY>
</TABLE>
<p>
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.
</p>
</BLOCKQUOTE>
<BR>
<BR>
<H3>Painting Threshold</H3>
<BLOCKQUOTE>
<P>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.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="Note" border="0"> Zooming past the painting
threshold will improve the rendering speed of the Primary View.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Saving"></A>Saving View Information</H2> <H2><A name="Saving"></A>Saving View Information</H2>
@ -1015,6 +881,19 @@
</BLOCKQUOTE> </BLOCKQUOTE>
<H2>Standard Graph Features and Actions</H2>
<BLOCKQUOTE>
<P>The function graph is a type of Ghidra Visual Graph and has some standard concepts, features
and actions.
<BLOCKQUOTE>
<UL>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Satellite_View">Satellite View</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Pan">Panning</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Zoom">Zooming</A></LI>
<LI><A href="help/topics/VisualGraph/Visual_Graph.html#Interaction_Threshold">Interaction Threshold</A></LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Function Graph Plugin</I></P> <P class="providedbyplugin">Provided by: <I>Function Graph Plugin</I></P>

View file

@ -173,7 +173,7 @@ public class FunctionGraph extends GroupingVisualGraph<FGVertex, FGEdge> {
@Override @Override
public void vertexLocationChanged(FGVertex v, Point point, ChangeType changeType) { public void vertexLocationChanged(FGVertex v, Point point, ChangeType changeType) {
if (changeType == ChangeType.USER) { if (!changeType.isTransitional()) {
settings.putVertexLocation(v, point); settings.putVertexLocation(v, point);
} }
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 com.google.common.base.Function;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer;
import ghidra.app.plugin.core.functiongraph.graph.FGEdge; import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph;
@ -56,7 +57,8 @@ public class EmptyLayout extends AbstractVisualGraphLayout<FGVertex, FGEdge> imp
} }
@Override @Override
public Function<FGEdge, Shape> getEdgeShapeTransformer() { public Function<FGEdge, Shape> getEdgeShapeTransformer(
RenderContext<FGVertex, FGEdge> context) {
return new ArticulatedEdgeTransformer<>(); return new ArticulatedEdgeTransformer<>();
} }

View file

@ -97,7 +97,7 @@ public abstract class AbstractFlowChartLayout<V extends VisualVertex, E extends
LayoutPositions<V, E> positions = LayoutPositions.createNewPositions(vertexMap, edgeMap); LayoutPositions<V, E> positions = LayoutPositions.createNewPositions(vertexMap, edgeMap);
// DEGUG triggers grid lines to be printed; useful for debugging // DEGUG triggers grid lines to be printed; useful for debugging
// VisualGraphRenderer.setGridPainter(new GridPainter(layoutMap.getGridCoordinates())); // VisualGraphRenderer.setGridPainter(new GridPainter(layoutMap.getGridCoordinates()));
layoutMap.dispose(); layoutMap.dispose();
return positions; return positions;

View file

@ -36,9 +36,9 @@ public class FlowChartLayoutTest extends AbstractFlowChartLayoutTest {
@Test @Test
public void testBasicRootWithTwoChildren() throws CancelledException { public void testBasicRootWithTwoChildren() throws CancelledException {
edge(A, B); edge(A, B);
edge(A, C); edge(A, C);
applyLayout();
showGraph(); showGraph();
assertVertices(""" assertVertices("""
@ -67,7 +67,7 @@ public class FlowChartLayoutTest extends AbstractFlowChartLayoutTest {
edge(A, D); edge(A, D);
applyLayout(); applyLayout();
// showGraph(); showGraph();
assertVertices(""" assertVertices("""
...... ......

View file

@ -21,8 +21,6 @@ import java.util.*;
import org.junit.Test; import org.junit.Test;
import ghidra.graph.graphs.TestEdge;
import ghidra.graph.graphs.TestVertex;
import ghidra.graph.viewer.layout.GridPoint; import ghidra.graph.viewer.layout.GridPoint;
public class EdgeSegmentTest { public class EdgeSegmentTest {

View file

@ -324,7 +324,7 @@ public class FcgVertexShapeProvider extends CircleWithLabelVertexShapeProvider {
} }
@Override @Override
protected void setTogglesVisible(boolean visible) { public void setTogglesVisible(boolean visible) {
boolean isIn = isInDirection(); boolean isIn = isInDirection();
boolean turnOn = isIn && hasIncomingReferences && visible; boolean turnOn = isIn && hasIncomingReferences && visible;

View file

@ -147,6 +147,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
} }
private void showContextMenu(MouseEvent e) { private void showContextMenu(MouseEvent e) {
if (e.isConsumed()) { if (e.isConsumed()) {
return; return;
} }

View file

@ -23,6 +23,7 @@ import javax.swing.*;
import docking.action.*; import docking.action.*;
import docking.menu.*; import docking.menu.*;
import generic.theme.CloseIcon;
import generic.theme.GColor; import generic.theme.GColor;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.task.SwingUpdateManager; import ghidra.util.task.SwingUpdateManager;
@ -32,7 +33,7 @@ import ghidra.util.task.SwingUpdateManager;
*/ */
class DockableToolBarManager { class DockableToolBarManager {
private static final Color BUTTON_COLOR = new GColor("color.fg.button"); 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 Icon MENU_ICON = new DropDownMenuIcon(BUTTON_COLOR);
private GenericHeader dockableHeader; private GenericHeader dockableHeader;
private ToolBarManager toolBarManager; private ToolBarManager toolBarManager;

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -277,6 +277,10 @@ public class GenericHeader extends JPanel {
titlePanel.setTitle(title); titlePanel.setTitle(title);
} }
public String getTitle() {
return titlePanel.getTitle();
}
public void setIcon(Icon icon) { public void setIcon(Icon icon) {
titlePanel.setIcon(icon); titlePanel.setIcon(icon);
} }
@ -396,6 +400,10 @@ public class GenericHeader extends JPanel {
titleLabel.setToolTipText(s); titleLabel.setToolTipText(s);
} }
String getTitle() {
return titleLabel.getText();
}
void setIcon(Icon icon) { void setIcon(Icon icon) {
icon = DockingUtils.scaleIconAsNeeded(icon); icon = DockingUtils.scaleIconAsNeeded(icon);

View file

@ -20,10 +20,9 @@ import java.awt.event.*;
import javax.swing.*; import javax.swing.*;
import docking.CloseIcon;
import docking.widgets.EmptyBorderButton; import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GDLabel; 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 * 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 { public class DockingTabRenderer extends JPanel {
private static final int MAX_TITLE_LENGTH = 25; 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 titleLabel;
private JLabel iconLabel; private JLabel iconLabel;

View file

@ -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 <T> the row data object type
*/
public abstract class AbstractGTrableRowModel<T> implements GTrableRowModel<T> {
private List<GTrableModeRowlListener> 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();
}
}
}

View file

@ -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 <T> the data model row object type
*/
public class DefaultGTrableCellRenderer<T> extends DefaultTableCellRenderer
implements GTrableCellRenderer<T> {
@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;
}
}

View file

@ -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 <T> the row object type
*/
public class DefaultGTrableRowModel<T extends GTrableRow<T>> extends AbstractGTrableRowModel<T> {
protected List<T> rows;
public DefaultGTrableRowModel(List<T> 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<T> children = row.getChildRows();
rows.addAll(lineIndex + 1, children);
row.setExpanded(true);
fireModelChanged();
return children.size();
}
}

View file

@ -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.
* <P>
* 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.
* <P>
* 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 <T> The row object type
*/
public class GTrable<T> 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<T> rowModel;
private GTrableColumnModel<T> columnModel;
private CellRendererPane renderPane;
private int selectedRow = -1;
private List<GTrableCellClickedListener> cellClickedListeners = new ArrayList<>();
private List<Consumer<Integer>> 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<T> rowModel, GTrableColumnModel<T> 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<T> 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<T> 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<Integer> 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<Integer> 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<Integer> 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<T, ?> 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<T, ?> column = columnModel.getColumn(i);
colWidth = column.getWidth();
paintColumn(g, x, y, colWidth, column, row, isSelected);
x += colWidth;
}
}
private <C> void paintColumn(Graphics g, int x, int y, int width, GTrableColumn<T, C> column,
T row, boolean isSelected) {
GTrableCellRenderer<C> 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<T, ?> 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;
}
}
}
}

View file

@ -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);
}

View file

@ -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 <C> the type of the column value for this cell
*/
public interface GTrableCellRenderer<C> {
/**
* 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);
}

View file

@ -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 <R> the row object type
* @param <C> the column value type
*/
public abstract class GTrableColumn<R, C> {
private static final int DEFAULT_MIN_SIZE = 20;
private static GTrableCellRenderer<Object> DEFAULT_RENDERER =
new DefaultGTrableCellRenderer<>();
private int startX;
private int width;
public GTrableColumn() {
width = getPreferredWidth();
}
public int getWidth() {
return width;
}
@SuppressWarnings("unchecked")
public GTrableCellRenderer<C> getRenderer() {
return (GTrableCellRenderer<C>) 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;
}
}

View file

@ -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 <T> the row object type
*/
public abstract class GTrableColumnModel<T> {
private List<GTrableColumn<T, ?>> 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<T, ?> 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<T, ?> column : columns) {
preferredWidth += column.getPreferredWidth();
}
return preferredWidth;
}
protected int computeWidth() {
int width = 0;
for (GTrableColumn<T, ?> 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<GTrableColumn<T, ?>> 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<T, ?> 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<T, ?> 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<T, ?> 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<T, ?> 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<T, ?> 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<T, ?> 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<T, ?> 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<T, ?> 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;
}
}

View file

@ -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();
}

View file

@ -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 <T> the row object type
*/
public abstract class GTrableRow<T extends GTrableRow<T>> {
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<T> getChildRows();
}

View file

@ -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 <T> the row object type
*/
public interface GTrableRowModel<T> {
/**
* {@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);
}

View file

@ -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;
}
}

View file

@ -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<TestDataRow> rowModel;
private TestColumnModel columnModel;
private GTrable<TestDataRow> gTrable;
private JFrame frame;
@Before
public void setUp() {
rowModel = createRowModel();
columnModel = new TestColumnModel();
gTrable = new GTrable<TestDataRow>(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<String> actualRows = getRowsAsText(0, rowModel.getRowCount() - 1);
assertEquals(expectedRows.length, actualRows.size());
List<String> expectedList = Arrays.asList(expectedRows);
assertListEqualOrdered(expectedList, actualRows);
}
private void assertVisibleRows(String... expectedRows) {
Range visibleRows = gTrable.getVisibleRows();
List<String> actualRows = getRowsAsText(visibleRows.min, visibleRows.max);
assertEquals(expectedRows.length, actualRows.size());
List<String> expectedList = Arrays.asList(expectedRows);
assertListEqualOrdered(expectedList, actualRows);
}
private List<String> getRowsAsText(int startRow, int endRow) {
List<String> 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<TestDataRow> 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<TestDataRow> {
private List<TestDataRow> children;
private String name;
protected TestDataRow(String name, int indentLevel, List<TestDataRow> children) {
super(indentLevel);
this.name = name;
this.children = children;
}
public String getName() {
return name;
}
@Override
public boolean isExpandable() {
return children != null;
}
@Override
protected List<TestDataRow> getChildRows() {
return children;
}
}
private class NameColumn extends GTrableColumn<TestDataRow, String> {
@Override
public String getValue(TestDataRow row) {
return row.getName();
}
@Override
protected int getPreferredWidth() {
return 150;
}
}
class TestColumnModel extends GTrableColumnModel<TestDataRow> {
@Override
protected void populateColumns(List<GTrableColumn<TestDataRow, ?>> columnList) {
columnList.add(new NameColumn());
}
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/javaProject.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/jacocoProject.gradle"
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle" apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
apply from: "$rootProject.projectDir/gradle/javadoc.gradle" apply from: "$rootProject.projectDir/gradle/javadoc.gradle"

View file

@ -14,6 +14,8 @@ src/main/docs/VisualGraphHierarchy.png||GHIDRA||||END|
src/main/docs/VisualGraphHierarchy.xml||GHIDRA||||END| src/main/docs/VisualGraphHierarchy.xml||GHIDRA||||END|
src/main/docs/VisualGraphViewer.png||GHIDRA||||END| src/main/docs/VisualGraphViewer.png||GHIDRA||||END|
src/main/docs/VisualGraphViewer.xml||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/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/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| 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|

View file

@ -0,0 +1,54 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
</tocroot>

View file

@ -0,0 +1,149 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Visual Graphs</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Visual_Graph"></A>Visual Graphs</H1>
<BLOCKQUOTE>
<P>Visual Graphs are highly integrated graphs that all share common features and
actions. They typically have both a <A href="#Visual_Graph_Primary_View">Primary View</A> and
a <A href="#Satellite_View">Satellite View</A>
</BLOCKQUOTE>
<H2><A name="Visual_Graph_Primary_View"></A>Primary View</H2>
<BLOCKQUOTE>
<P>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.</P>
</BLOCKQUOTE>
<H2><A name="Satellite_View"></A>Satellite View</H2>
<BLOCKQUOTE>
<P>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 <B>lens</B> (the white rectangle) that indicates how much of the
current graph fits into the primary view.</P>
<P>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
<IMG src= "images/network-wireless.png" alt="" border="1">icon in the corner to indicate that
it is available.</P>
<P>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.</P>
<P>You may show/hide the satellite view by right-clicking anywhere in the Primary View and
selecting or deselecting the <B>Display Satellite View</B> toggle button from the popup
menu.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> If the Primary View is painting
sluggishly, then hiding the Satellite View cause the Primary View to be more
responsive.</P>
</BLOCKQUOTE>
<H3><A name="Satellite_View_Dock"></A>Detached Satellite</H3>
<BLOCKQUOTE>
<P>The Satellite View is docked by default. However, you can choose to instead
undock it and display it in its own window.</P>
<P>To undock the Satellite View, right-click in the graph and deselect the <B>Dock
Satellite View</B> menu item.</P>
<P>To re-dock the Satellite View, right-click in the graph and select the <B>Dock Satellite
View</B> menu item.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> To show the Satellite View if it is
hidden, whether docked or undocked, you can press the <IMG src=
"images/network-wireless.png" alt="" border="1"> 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.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H3><A name="Satellite_Location"></A>Docked Satellite Location</H3>
<BLOCKQUOTE>
<P>When the Satellite View is attached, or <B>docked</B>, to the Primary View, you can
choose which corner to show the satellite view. To change the corner, right-click in the
graph, select <B>Docked Satellite Position</B> and then select the appropriate sub-menu for
the desired corner.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2>Standar Graph Operations</H2>
<BLOCKQUOTE>
<H3><A name="Pan"></A>Panning</H3>
<BLOCKQUOTE>
<P>There are various ways to move the graph. To move the graph in any direction you can
drag from the whitespace of the graph.</P>
<P>By default, the scroll wheel zooms the graph. The scroll wheel can also be used
to scroll the graph vertically by holding the <TT><B>Ctrl</B></TT> key while
using the scroll wheel. Alternatively, you can move the graph left to right using the
mouse while holding <TT><B>Ctrl-Alt</B></TT>.</P>
<P>The satellite viewer may also be used to move the primary graphs view by dragging and
clicking inside of the satellite viewer.</P>
</BLOCKQUOTE>
<H3><A name="Zoom"></A>Zooming</H3>
<BLOCKQUOTE>
<P>At <B>full zoom</B>, or <B>block level zoom</B>, 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.</P>
<BLOCKQUOTE>
<P>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.</P>
</BLOCKQUOTE>
<P>The <A href="#Satellite_View">satellite viewer</A> is always zoomed out far enough to
fit the entire graph into its window.</P>
</BLOCKQUOTE>
<H3><A name="Interaction_Threshold"></A>Interaction Threshold</H3>
<BLOCKQUOTE>
<P>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.</P>
</BLOCKQUOTE>
<H3>Painting Threshold</H3>
<BLOCKQUOTE>
<P>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.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BR>
<BR>
</BODY>
</HTML>

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.VgSatelliteFeaturette;
import ghidra.graph.featurette.VisualGraphFeaturette; import ghidra.graph.featurette.VisualGraphFeaturette;
import ghidra.graph.viewer.*; import ghidra.graph.viewer.*;
import ghidra.graph.viewer.GraphComponent.SatellitePosition;
import ghidra.graph.viewer.actions.*; import ghidra.graph.viewer.actions.*;
import ghidra.graph.viewer.event.mouse.VertexMouseInfo; import ghidra.graph.viewer.event.mouse.VertexMouseInfo;
@ -137,11 +138,16 @@ public abstract class VisualGraphComponentProvider<V extends VisualVertex,
* Adds the satellite viewer functionality to this provider * Adds the satellite viewer functionality to this provider
*/ */
protected void addSatelliteFeature() { protected void addSatelliteFeature() {
addSatelliteFeature(true, SatellitePosition.LOWER_RIGHT);
}
protected void addSatelliteFeature(boolean satelliteVisible, SatellitePosition position) {
VgSatelliteFeaturette<V, E, G> satelliteFeature = new VgSatelliteFeaturette<>(); VgSatelliteFeaturette<V, E, G> satelliteFeature = new VgSatelliteFeaturette<>();
satelliteFeature.init(this); satelliteFeature.init(this);
subFeatures.add(satelliteFeature); subFeatures.add(satelliteFeature);
satelliteFeature.setSatellitePosition(position);
satelliteFeature.setSatelliteVisible(satelliteVisible);
} }
/* /*
Features to provide Features to provide

View file

@ -97,27 +97,11 @@ public class VgSatelliteFeaturette<V extends VisualVertex,
view.setSatelliteDocked(dockSatellite); view.setSatelliteDocked(dockSatellite);
boolean showSatellite = saveState.getBoolean(DISPLAY_SATELLITE, true); boolean showSatellite = saveState.getBoolean(DISPLAY_SATELLITE, true);
toggleSatelliteAction.setSelected(showSatellite); setSatelliteVisible(showSatellite);
view.setSatelliteVisible(showSatellite);
String positionString = saveState.getString(DOCK_SATELLITE_POSITION, LOWER_RIGHT.name()); String positionString = saveState.getString(DOCK_SATELLITE_POSITION, LOWER_RIGHT.name());
SatellitePosition position = SatellitePosition.valueOf(positionString); SatellitePosition position = SatellitePosition.valueOf(positionString);
view.setSatellitePosition(position); setSatellitePosition(position);
deselectAllSatellitePositions();
switch (position) {
case LOWER_LEFT:
lowerLeftAction.setSelected(true);
break;
case LOWER_RIGHT:
lowerRightAction.setSelected(true);
break;
case UPPER_LEFT:
upperLeftAction.setSelected(true);
break;
case UPPER_RIGHT:
upperRightAction.setSelected(true);
break;
}
} }
@Override @Override
@ -183,7 +167,7 @@ public class VgSatelliteFeaturette<V extends VisualVertex,
toggleSatelliteAction.setPopupMenuData( toggleSatelliteAction.setPopupMenuData(
new MenuData(new String[] { "Display Satellite View" })); new MenuData(new String[] { "Display Satellite View" }));
toggleSatelliteAction.setHelpLocation( toggleSatelliteAction.setHelpLocation(
new HelpLocation("FunctionCallGraphPlugin", "Satellite_View")); new HelpLocation("VisualGraph", "Satellite_View"));
dockSatelliteAction = new ToggleDockingAction("Dock Satellite View", owner) { dockSatelliteAction = new ToggleDockingAction("Dock Satellite View", owner) {
@Override @Override
@ -208,8 +192,7 @@ public class VgSatelliteFeaturette<V extends VisualVertex,
}; };
dockSatelliteAction.setSelected(true); dockSatelliteAction.setSelected(true);
dockSatelliteAction.setPopupMenuData(new MenuData(new String[] { "Dock Satellite View" })); dockSatelliteAction.setPopupMenuData(new MenuData(new String[] { "Dock Satellite View" }));
dockSatelliteAction.setHelpLocation( dockSatelliteAction.setHelpLocation(new HelpLocation("VisualGraph", "Satellite_View"));
new HelpLocation("FunctionCallGraphPlugin", "Satellite_View"));
upperLeftAction = new SatellitePositionAction("Upper Left", UPPER_LEFT, provider); upperLeftAction = new SatellitePositionAction("Upper Left", UPPER_LEFT, provider);
upperRightAction = new SatellitePositionAction("Upper Right", UPPER_RIGHT, provider); upperRightAction = new SatellitePositionAction("Upper Right", UPPER_RIGHT, provider);
@ -234,7 +217,31 @@ public class VgSatelliteFeaturette<V extends VisualVertex,
} }
} }
public void deselectAllSatellitePositions() { public void setSatelliteVisible(boolean visible) {
toggleSatelliteAction.setSelected(visible);
view.setSatelliteVisible(visible);
}
public void setSatellitePosition(SatellitePosition position) {
deselectAllSatellitePositions();
switch (position) {
case LOWER_LEFT:
lowerLeftAction.setSelected(true);
break;
case LOWER_RIGHT:
lowerRightAction.setSelected(true);
break;
case UPPER_LEFT:
upperLeftAction.setSelected(true);
break;
case UPPER_RIGHT:
upperRightAction.setSelected(true);
break;
}
view.setSatellitePosition(position);
}
void deselectAllSatellitePositions() {
upperLeftAction.setSelected(false); upperLeftAction.setSelected(false);
upperRightAction.setSelected(false); upperRightAction.setSelected(false);
lowerLeftAction.setSelected(false); lowerLeftAction.setSelected(false);
@ -355,7 +362,7 @@ public class VgSatelliteFeaturette<V extends VisualVertex,
this.position = posiiton; this.position = posiiton;
this.provider = provider; this.provider = provider;
setPopupMenuData(new MenuData(new String[] { "Docked Satellite Position", name })); setPopupMenuData(new MenuData(new String[] { "Docked Satellite Position", name }));
setHelpLocation(new HelpLocation("FunctionCallGraphPlugin", "Satellite_Location")); setHelpLocation(new HelpLocation("VisualGraph", "Satellite_Location"));
} }
@Override @Override

View file

@ -0,0 +1,78 @@
/* ###
* 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.Point;
import java.awt.geom.Point2D;
import edu.uci.ics.jung.visualization.*;
import ghidra.graph.viewer.*;
public class RelayoutAndCenterVertexGraphJob<V extends VisualVertex, E extends VisualEdge<V>>
extends RelayoutFunctionGraphJob<V, E> {
private V vertex;
private Point2D destination;
private Point2D lastPoint = new Point2D.Double(0, 0);
public RelayoutAndCenterVertexGraphJob(GraphViewer<V, E> 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<V, E> 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();
}
}

View file

@ -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 <V> the vertex type
* @param <E> the edge type
*/
public class RelayoutAndEnsureVisible<V extends VisualVertex, E extends VisualEdge<V>>
extends RelayoutFunctionGraphJob<V, E> {
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<V, E> 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<V, E> 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);
}
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.*;
import java.util.Map.Entry; 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.*;
import ghidra.graph.viewer.layout.LayoutPositions; import ghidra.graph.viewer.layout.LayoutPositions;
@ -32,25 +29,6 @@ public class RelayoutFunctionGraphJob<V extends VisualVertex, E extends VisualEd
super(viewer, useAnimation); super(viewer, useAnimation);
} }
@Override
protected Animator createAnimator() {
initializeVertexLocations();
clearLocationCache();
if (!useAnimation) {
return null;
}
updateOpacity(0);
Animator newAnimator =
PropertySetter.createAnimator(duration, this, "percentComplete", 0.0, 1.0);
newAnimator.setAcceleration(0f);
newAnimator.setDeceleration(0.8f);
return newAnimator;
}
@Override @Override
protected void finished() { protected void finished() {
@ -77,6 +55,7 @@ public class RelayoutFunctionGraphJob<V extends VisualVertex, E extends VisualEd
// Create the new vertex locations. // Create the new vertex locations.
// //
Collection<V> vertices = graph.getVertices(); Collection<V> vertices = graph.getVertices();
for (V vertex : vertices) { for (V vertex : vertices) {
Point2D currentPoint = toLocation(vertex); Point2D currentPoint = toLocation(vertex);
Point2D startPoint = (Point2D) currentPoint.clone(); Point2D startPoint = (Point2D) currentPoint.clone();

View file

@ -293,7 +293,7 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
// the layout defines the shape of the edge (this gives the layout flexibility in how // the layout defines the shape of the edge (this gives the layout flexibility in how
// to render its shape) // to render its shape)
Function<E, Shape> edgeTransformer = layout.getEdgeShapeTransformer(); Function<E, Shape> edgeTransformer = layout.getEdgeShapeTransformer(renderContext);
renderContext.setEdgeShapeTransformer(edgeTransformer); renderContext.setEdgeShapeTransformer(edgeTransformer);
renderContext.setArrowPlacementTolerance(5.0f); renderContext.setArrowPlacementTolerance(5.0f);
@ -361,7 +361,7 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
visualEdgeRenderer.setHoveredColorTransformer( visualEdgeRenderer.setHoveredColorTransformer(
e -> new GColor("color.visualgraph.view.satellite.edge.hovered")); e -> new GColor("color.visualgraph.view.satellite.edge.hovered"));
Function<E, Shape> edgeTransformer = layout.getEdgeShapeTransformer(); Function<E, Shape> edgeTransformer = layout.getEdgeShapeTransformer(renderContext);
renderContext.setEdgeShapeTransformer(edgeTransformer); renderContext.setEdgeShapeTransformer(edgeTransformer);
renderContext.setVertexShapeTransformer(new VisualGraphVertexShapeTransformer<>()); renderContext.setVertexShapeTransformer(new VisualGraphVertexShapeTransformer<>());
@ -471,14 +471,9 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
button.setOpaque(false); button.setOpaque(false);
button.setToolTipText(tooltip); button.setToolTipText(tooltip);
/*
TODO fix when the Generic Visual Graph help module is created
HelpService helpService = DockingWindowManager.getHelpService(); HelpService helpService = DockingWindowManager.getHelpService();
helpService.registerHelp(button, helpService.registerHelp(button,
new HelpLocation("GraphTopic", "Satellite_View_Dock")); new HelpLocation("Visual_Graph", "Satellite_View_Dock"));
*/
return button; return button;
} }
@ -1011,7 +1006,7 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
v.setLocation(newLocation); v.setLocation(newLocation);
if (changeType == ChangeType.RESTORE) { if (changeType.isTransitional()) {
// ignore these events, as they are a bulk operation and will be handled later // ignore these events, as they are a bulk operation and will be handled later
return; return;
} }
@ -1183,6 +1178,12 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
@Override @Override
public void verticesRemoved(Iterable<V> vertices) { public void verticesRemoved(Iterable<V> vertices) {
getPathHighlighter().clearEdgeCache(); getPathHighlighter().clearEdgeCache();
// clear any deleted nodes from the pick state
PickedState<V> pickedState = primaryViewer.getPickedVertexState();
for (V v : vertices) {
pickedState.pick(v, false);
}
} }
@Override @Override

View file

@ -117,9 +117,15 @@ public class GraphViewerUtils {
public static <V, E> Point getVertexUpperLeftCornerInLayoutSpace( public static <V, E> Point getVertexUpperLeftCornerInLayoutSpace(
VisualizationServer<V, E> viewer, V vertex) { VisualizationServer<V, E> viewer, V vertex) {
Point vertexCenterInLayoutSpace = getVertexCenterPointInLayoutSpace(viewer, vertex);
Point vertexGraphSpaceLocation = getVertexUpperLeftCornerInGraphSpace(viewer, vertex); RenderContext<V, E> renderContext = viewer.getRenderContext();
return translatePointFromGraphSpaceToLayoutSpace(vertexGraphSpaceLocation, viewer); 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 <V, E> Point getVertexUpperLeftCornerInViewSpace(VisualizationServer<V, E> viewer, public static <V, E> Point getVertexUpperLeftCornerInViewSpace(VisualizationServer<V, E> viewer,
@ -215,17 +221,8 @@ public class GraphViewerUtils {
public static <V, E> Point getVertexUpperLeftCornerInGraphSpace( public static <V, E> Point getVertexUpperLeftCornerInGraphSpace(
VisualizationServer<V, E> viewer, V vertex) { VisualizationServer<V, E> viewer, V vertex) {
Point vertexCenterInLayoutSpace = getVertexCenterPointInLayoutSpace(viewer, vertex); Point layoutPoint = getVertexUpperLeftCornerInLayoutSpace(viewer, vertex);
Point vertexCenterInGraphSpace = return translatePointFromLayoutSpaceToGraphSpace(layoutPoint, viewer);
translatePointFromLayoutSpaceToGraphSpace(vertexCenterInLayoutSpace, viewer);
RenderContext<V, E> 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);
} }
public static <V, E> Point translatePointFromLayoutSpaceToGraphSpace(Point2D pointInLayoutSpace, public static <V, E> Point translatePointFromLayoutSpaceToGraphSpace(Point2D pointInLayoutSpace,
@ -688,32 +685,11 @@ public class GraphViewerUtils {
double endY = (float) endVertexCenter.getY(); double endY = (float) endVertexCenter.getY();
RenderContext<V, E> renderContext = viewer.getRenderContext(); RenderContext<V, E> 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<? super V, Shape> vertexShapeTransformer =
renderContext.getVertexShapeTransformer();
Shape vertexShape = getVertexShapeForEdge(endVertex, vertexShapeTransformer);
return createHollowEgdeLoopInGraphSpace(vertexShape, startX, startY);
}
// translate the edge from 0,0 to the starting vertex point // translate the edge from 0,0 to the starting vertex point
AffineTransform xform = AffineTransform.getTranslateInstance(startX, startY); AffineTransform xform = AffineTransform.getTranslateInstance(startX, startY);
Shape edgeShape = renderContext.getEdgeShapeTransformer().apply(e); 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 // apply the transformations; converting the given shape from model space into graph space
return xform.createTransformedShape(edgeShape); return xform.createTransformedShape(edgeShape);
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.*;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.util.Objects; import java.util.Objects;
@ -46,9 +47,11 @@ public class VertexMouseInfo<V extends VisualVertex, E extends VisualEdge<V>> {
private final V vertex; private final V vertex;
private MouseEvent translatedMouseEvent; private MouseEvent translatedMouseEvent;
private Component mousedDestinationComponent; private Component mousedDestinationComponent;
private Point2D vertexBasedClickPoint;
public VertexMouseInfo(MouseEvent originalMouseEvent, V vertex, Point2D vertexBasedClickPoint, public VertexMouseInfo(MouseEvent originalMouseEvent, V vertex, Point2D vertexBasedClickPoint,
GraphViewer<V, E> viewer) { GraphViewer<V, E> viewer) {
this.vertexBasedClickPoint = vertexBasedClickPoint;
this.originalMouseEvent = Objects.requireNonNull(originalMouseEvent); this.originalMouseEvent = Objects.requireNonNull(originalMouseEvent);
this.vertex = Objects.requireNonNull(vertex); this.vertex = Objects.requireNonNull(vertex);
this.viewer = Objects.requireNonNull(viewer); this.viewer = Objects.requireNonNull(viewer);
@ -59,6 +62,10 @@ public class VertexMouseInfo<V extends VisualVertex, E extends VisualEdge<V>> {
setClickedComponent(deepestComponent, vertexBasedClickPoint); setClickedComponent(deepestComponent, vertexBasedClickPoint);
} }
public Point2D getVertexRelativeClickPoint() {
return vertexBasedClickPoint;
}
public boolean isScaledPastInteractionThreshold() { public boolean isScaledPastInteractionThreshold() {
RenderContext<V, E> renderContext = viewer.getRenderContext(); RenderContext<V, E> renderContext = viewer.getRenderContext();
MultiLayerTransformer multiLayerTransformer = renderContext.getMultiLayerTransformer(); MultiLayerTransformer multiLayerTransformer = renderContext.getMultiLayerTransformer();
@ -75,6 +82,11 @@ public class VertexMouseInfo<V extends VisualVertex, E extends VisualEdge<V>> {
if (!isVertexSelected()) { if (!isVertexSelected()) {
return HAND_CURSOR; return HAND_CURSOR;
} }
if (mousedDestinationComponent != null) {
return mousedDestinationComponent.getCursor();
}
return DEFAULT_CURSOR; return DEFAULT_CURSOR;
} }
@ -221,12 +233,22 @@ public class VertexMouseInfo<V extends VisualVertex, E extends VisualEdge<V>> {
System.currentTimeMillis(), 0, 0, 0, 0, false); System.currentTimeMillis(), 0, 0, 0, 0, false);
} }
private MouseEvent createMouseEventFromSource(Component source, MouseEvent progenitor, private MouseEvent createMouseEventFromSource(Component source, MouseEvent ev,
Point2D clickPoint) { Point2D clickPoint) {
return new MouseEvent(source, progenitor.getID(), progenitor.getWhen(), if (ev instanceof MouseWheelEvent wheelEvent) {
progenitor.getModifiers() | progenitor.getModifiersEx(), (int) clickPoint.getX(), int scrollType = wheelEvent.getScrollType();
(int) clickPoint.getY(), progenitor.getClickCount(), progenitor.isPopupTrigger(), int scrollAmount = wheelEvent.getScrollAmount();
progenitor.getButton()); 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() { public boolean isPopupClick() {

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -265,6 +265,7 @@ public class VisualGraphEventForwardingGraphMousePlugin<V extends VisualVertex,
currentMouseEnteredInfo = mouseMovedMouseInfo; currentMouseEnteredInfo = mouseMovedMouseInfo;
mouseMovedMouseInfo.forwardEvent(); mouseMovedMouseInfo.forwardEvent();
updateCursor(mouseMovedMouseInfo);
} }
private void triggerMouseExited(VertexMouseInfo<V, E> currentInfo, private void triggerMouseExited(VertexMouseInfo<V, E> currentInfo,

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.*; import edu.uci.ics.jung.visualization.*;
import edu.uci.ics.jung.visualization.layout.ObservableCachingLayout;
import edu.uci.ics.jung.visualization.picking.PickedState; import edu.uci.ics.jung.visualization.picking.PickedState;
import ghidra.graph.viewer.*; import ghidra.graph.viewer.*;
import ghidra.graph.viewer.layout.LayoutListener.ChangeType;
import ghidra.graph.viewer.layout.VisualGraphLayout;
public class VisualGraphPickingGraphMousePlugin<V extends VisualVertex, E extends VisualEdge<V>> public class VisualGraphPickingGraphMousePlugin<V extends VisualVertex, E extends VisualEdge<V>>
extends JungPickingGraphMousePlugin<V, E> implements VisualGraphMousePlugin<V, E> { extends JungPickingGraphMousePlugin<V, E> implements VisualGraphMousePlugin<V, E> {
@ -100,7 +103,13 @@ public class VisualGraphPickingGraphMousePlugin<V extends VisualVertex, E extend
for (V v : ps.getPicked()) { for (V v : ps.getPicked()) {
Point2D vertexPoint = layout.apply(v); Point2D vertexPoint = layout.apply(v);
vertexPoint.setLocation(vertexPoint.getX() + dx, vertexPoint.getY() + dy); vertexPoint.setLocation(vertexPoint.getX() + dx, vertexPoint.getY() + dy);
layout.setLocation(v, vertexPoint); VisualGraphLayout<V, E> vgLayout = getVisualGraphLayout(layout);
if (vgLayout != null) {
vgLayout.setLocation(v, vertexPoint, ChangeType.USER);
}
else {
layout.setLocation(v, vertexPoint);
}
updatedArticulatedEdges(viewer, v); updatedArticulatedEdges(viewer, v);
} }
@ -108,6 +117,16 @@ public class VisualGraphPickingGraphMousePlugin<V extends VisualVertex, E extend
e.consume(); e.consume();
} }
private VisualGraphLayout<V, E> getVisualGraphLayout(Layout<V, E> layout) {
if (layout instanceof VisualGraphLayout<V, E> vgLayout) {
return vgLayout;
}
if (layout instanceof ObservableCachingLayout<V, E> observable) {
return getVisualGraphLayout(observable.getDelegate());
}
return null;
}
private void updatedArticulatedEdges(GraphViewer<V, E> viewer, V v) { private void updatedArticulatedEdges(GraphViewer<V, E> viewer, V v) {
Layout<V, E> layout = viewer.getGraphLayout(); Layout<V, E> layout = viewer.getGraphLayout();

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -15,6 +15,7 @@
*/ */
package ghidra.graph.viewer.event.mouse; package ghidra.graph.viewer.event.mouse;
import java.awt.Component;
import java.awt.event.*; import java.awt.event.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -113,14 +114,15 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseListener)) { if (!(p instanceof MouseListener)) {
continue; continue;
} }
trace("mouseClicked() on " + p, copy);
trace("mouseClicked() on " + p, e); ((MouseListener) p).mouseClicked(copy);
((MouseListener) p).mouseClicked(e); if (copy.isConsumed()) {
if (e.isConsumed()) { e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -129,14 +131,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseListener)) { if (!(p instanceof MouseListener)) {
continue; continue;
} }
trace("mousePressed() on " + p, e); trace("mousePressed() on " + p, copy);
((MouseListener) p).mousePressed(e); ((MouseListener) p).mousePressed(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -145,14 +149,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseListener)) { if (!(p instanceof MouseListener)) {
continue; continue;
} }
trace("mouseReleased() on " + p, e); trace("mouseReleased() on " + p, copy);
((MouseListener) p).mouseReleased(e); ((MouseListener) p).mouseReleased(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -161,14 +167,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseEntered(MouseEvent e) { public void mouseEntered(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseListener)) { if (!(p instanceof MouseListener)) {
continue; continue;
} }
trace("mouseEntered() on " + p, e); trace("mouseEntered() on " + p, copy);
((MouseListener) p).mouseEntered(e); ((MouseListener) p).mouseEntered(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -177,14 +185,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseExited(MouseEvent e) { public void mouseExited(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseListener)) { if (!(p instanceof MouseListener)) {
continue; continue;
} }
trace("mouseExited() on " + p, e); trace("mouseExited() on " + p, copy);
((MouseListener) p).mouseExited(e); ((MouseListener) p).mouseExited(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -193,14 +203,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseDragged(MouseEvent e) { public void mouseDragged(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseMotionListener)) { if (!(p instanceof MouseMotionListener)) {
continue; continue;
} }
trace("mouseDragged() on " + p, e); trace("mouseDragged() on " + p, copy);
((MouseMotionListener) p).mouseDragged(e); ((MouseMotionListener) p).mouseDragged(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -209,14 +221,16 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseMoved(MouseEvent e) { public void mouseMoved(MouseEvent e) {
MouseEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseMotionListener)) { if (!(p instanceof MouseMotionListener)) {
continue; continue;
} }
trace("mouseMoved() on " + p, e); trace("mouseMoved() on " + p, copy);
((MouseMotionListener) p).mouseMoved(e); ((MouseMotionListener) p).mouseMoved(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
@ -225,17 +239,59 @@ public class VisualGraphPluggableGraphMouse<V extends VisualVertex, E extends Vi
@Override @Override
public void mouseWheelMoved(MouseWheelEvent e) { public void mouseWheelMoved(MouseWheelEvent e) {
MouseWheelEvent copy = copy(e);
for (GraphMousePlugin p : mousePlugins) { for (GraphMousePlugin p : mousePlugins) {
if (!(p instanceof MouseWheelListener)) { if (!(p instanceof MouseWheelListener)) {
continue; continue;
} }
trace("mouseWheelMoved() on " + p, e); trace("mouseWheelMoved() on " + p, copy);
((MouseWheelListener) p).mouseWheelMoved(e); ((MouseWheelListener) p).mouseWheelMoved(copy);
if (e.isConsumed()) { if (copy.isConsumed()) {
e.consume();
trace("\tconsumed"); trace("\tconsumed");
return; return;
} }
} }
} }
/**
* Copies the given mouse event. We do this so that we allow our mouse plugins to process
* mouse events. This was done specifically allow us to update state when user right-clicks.
* Ghidra has code that will consume mouse clicks before we get the event.
* <P>
* 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);
}
} }

View file

@ -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.AbstractLayout;
import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph; 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.BasicEdgeRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel; import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel;
import ghidra.graph.VisualGraph; import ghidra.graph.VisualGraph;
@ -126,7 +127,8 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
} }
@Override @Override
public Function<E, Shape> getEdgeShapeTransformer() { public Function<E, Shape> getEdgeShapeTransformer(RenderContext<V, E> context) {
edgeShapeTransformer.setRenderContext(context);
return edgeShapeTransformer; return edgeShapeTransformer;
} }
@ -282,7 +284,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
for (Entry<V, Point2D> entry : entrySet) { for (Entry<V, Point2D> entry : entrySet) {
V vertex = entry.getKey(); V vertex = entry.getKey();
Point2D location = entry.getValue(); Point2D location = entry.getValue();
setLocation(vertex, location); setLocation(vertex, location, ChangeType.RESTORE);
vertex.setLocation(location); vertex.setLocation(location);
} }
} }
@ -698,10 +700,10 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
} }
private void fireVertexLocationChanged(V v, Point2D p) { private void fireVertexLocationChanged(V v, Point2D p) {
fireVertexLocationChanged(v, p, ChangeType.USER); fireVertexLocationChanged(v, p, ChangeType.API);
} }
private void fireVertexLocationChanged(V v, Point2D p, ChangeType type) { protected void fireVertexLocationChanged(V v, Point2D p, ChangeType type) {
Iterator<LayoutListener<V, E>> iterator = listeners.iterator(); Iterator<LayoutListener<V, E>> iterator = listeners.iterator();
for (; iterator.hasNext();) { for (; iterator.hasNext();) {
LayoutListener<V, E> layoutListener = iterator.next(); LayoutListener<V, E> layoutListener = iterator.next();

View file

@ -97,4 +97,14 @@ public class GridBounds {
return true; return true;
} }
public void transpose() {
int temp = minRow;
minRow = minCol;
minCol = temp;
temp = maxRow;
maxRow = maxCol;
maxCol = temp;
}
} }

View file

@ -44,13 +44,13 @@ public class GridLocationMap<V, E> {
protected Map<E, List<GridPoint>> edgePoints = new HashMap<>(); protected Map<E, List<GridPoint>> edgePoints = new HashMap<>();
private GridBounds gridBounds = new GridBounds(); private GridBounds gridBounds = new GridBounds();
// Tree based algorithms might want to track the column of the root node as it changes 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 // the grid is shifted or merged. Useful for determining the position of a parent node when
// building bottom up. // building bottom up.
private int rootColumn = 0; private GridPoint rootPoint;
public GridLocationMap() { public GridLocationMap() {
rootColumn = 0; rootPoint = new GridPoint(0, 0);
} }
/** /**
@ -60,7 +60,7 @@ public class GridLocationMap<V, E> {
* @param col the column for the initial vertex. * @param col the column for the initial vertex.
*/ */
public GridLocationMap(V root, int row, int col) { public GridLocationMap(V root, int row, int col) {
this.rootColumn = col; rootPoint = new GridPoint(row, col);
set(root, new GridPoint(row, col)); set(root, new GridPoint(row, col));
} }
@ -69,7 +69,15 @@ public class GridLocationMap<V, E> {
* @return the column of the initial vertex in this grid * @return the column of the initial vertex in this grid
*/ */
public int getRootColumn() { 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<V> vertices() { public Set<V> vertices() {
@ -245,7 +253,8 @@ public class GridLocationMap<V, E> {
p.row += rowShift; p.row += rowShift;
p.col += colShift; p.col += colShift;
} }
rootColumn += colShift; rootPoint.row += rowShift;
rootPoint.col += colShift;
gridBounds.shift(rowShift, colShift); gridBounds.shift(rowShift, colShift);
} }
@ -292,6 +301,30 @@ public class GridLocationMap<V, E> {
return rowRanges; 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) { public boolean containsVertex(V v) {
return vertexPoints.containsKey(v); return vertexPoints.containsKey(v);
} }
@ -371,7 +404,7 @@ public class GridLocationMap<V, E> {
private GridLocationMap<V, E> copy() { private GridLocationMap<V, E> copy() {
GridLocationMap<V, E> map = new GridLocationMap<>(); GridLocationMap<V, E> map = new GridLocationMap<>();
map.rootColumn = rootColumn; map.rootPoint = new GridPoint(rootPoint.row, rootPoint.col);
Set<Entry<V, GridPoint>> entries = vertexPoints.entrySet(); Set<Entry<V, GridPoint>> entries = vertexPoints.entrySet();
for (Entry<V, GridPoint> entry : entries) { for (Entry<V, GridPoint> entry : entries) {

View file

@ -57,6 +57,12 @@ public class GridPoint {
return col == other.col && row == other.row; return col == other.col && row == other.row;
} }
public void transpose() {
int temp = row;
row = col;
col = temp;
}
@Override @Override
public String toString() { public String toString() {
return "(r=" + row + ",c=" + col + ")"; return "(r=" + row + ",c=" + col + ")";

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph; 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.BasicEdgeRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel; import edu.uci.ics.jung.visualization.renderers.Renderer.EdgeLabel;
import ghidra.graph.VisualGraph; import ghidra.graph.VisualGraph;
@ -182,7 +183,8 @@ public class JungWrappingVisualGraphLayoutAdapter<V extends VisualVertex,
} }
@Override @Override
public Function<E, Shape> getEdgeShapeTransformer() { public Function<E, Shape> getEdgeShapeTransformer(RenderContext<V, E> context) {
edgeShapeTransformer.setRenderContext(context);
return edgeShapeTransformer; return edgeShapeTransformer;
} }
@ -239,7 +241,7 @@ public class JungWrappingVisualGraphLayoutAdapter<V extends VisualVertex,
@Override @Override
public void setLocation(V v, Point2D location) { public void setLocation(V v, Point2D location) {
delegate.setLocation(v, location); delegate.setLocation(v, location);
fireVertexLocationChanged(v, location, ChangeType.USER); fireVertexLocationChanged(v, location, ChangeType.API);
} }
@Override @Override

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -26,9 +26,14 @@ import java.awt.geom.Point2D;
public interface LayoutListener<V, E> { public interface LayoutListener<V, E> {
public enum ChangeType { 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 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;
}
} }
/** /**

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -107,4 +107,9 @@ public class LayoutPositions<V extends VisualVertex, E extends VisualEdge<V>> {
vertexLocations.clear(); vertexLocations.clear();
edgeArticulations.clear(); edgeArticulations.clear();
} }
@Override
public String toString() {
return vertexLocations.toString();
}
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 com.google.common.base.Function;
import edu.uci.ics.jung.algorithms.layout.Layout; 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.BasicEdgeRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer; import edu.uci.ics.jung.visualization.renderers.Renderer;
import ghidra.graph.VisualGraph; import ghidra.graph.VisualGraph;
@ -91,7 +92,9 @@ public interface VisualGraphLayout<V extends VisualVertex,
public VisualGraphLayout<V, E> cloneLayout(VisualGraph<V, E> newGraph); public VisualGraphLayout<V, E> cloneLayout(VisualGraph<V, E> 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.
* <P>
* Calling {@link #setLocation(Object, Point2D)} will use {@link ChangeType#API}.
* *
* @param v the vertex * @param v the vertex
* @param location the new location * @param location the new location
@ -116,9 +119,10 @@ public interface VisualGraphLayout<V extends VisualVertex,
/** /**
* Returns an optional edge shape transformer. This is used to create shapes for each edge. * Returns an optional edge shape transformer. This is used to create shapes for each edge.
* *
* @param context RenderContext needed to get rendering information
* @return an optional edge shape transformer * @return an optional edge shape transformer
*/ */
public Function<E, Shape> getEdgeShapeTransformer(); public Function<E, Shape> getEdgeShapeTransformer(RenderContext<V, E> context);
/** /**
* Returns an optional custom edge label renderer. This is used to add labels to the edges. * Returns an optional custom edge label renderer. This is used to add labels to the edges.

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,12 +16,15 @@
package ghidra.graph.viewer.shape; package ghidra.graph.viewer.shape;
import java.awt.Shape; import java.awt.Shape;
import java.awt.geom.*; import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.List; import java.util.List;
import com.google.common.base.Function; import com.google.common.base.Function;
import edu.uci.ics.jung.visualization.RenderContext;
import ghidra.graph.viewer.*; import ghidra.graph.viewer.*;
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
@ -33,6 +36,12 @@ import ghidra.util.SystemUtilities;
public class ArticulatedEdgeTransformer<V extends VisualVertex, E extends VisualEdge<V>> public class ArticulatedEdgeTransformer<V extends VisualVertex, E extends VisualEdge<V>>
implements Function<E, Shape> { implements Function<E, Shape> {
private RenderContext<V, E> renderContext;
public void setRenderContext(RenderContext<V, E> context) {
this.renderContext = context;
}
/** /**
* Get the shape for this edge * Get the shape for this edge
* *
@ -45,6 +54,21 @@ public class ArticulatedEdgeTransformer<V extends VisualVertex, E extends Visual
V end = e.getEnd(); V end = e.getEnd();
boolean isLoop = start.equals(end); boolean isLoop = start.equals(end);
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<? super V, Shape> 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) { if (isLoop) {
return GraphViewerUtils.createHollowEgdeLoop(); return GraphViewerUtils.createHollowEgdeLoop();
} }
@ -82,43 +106,23 @@ public class ArticulatedEdgeTransformer<V extends VisualVertex, E extends Visual
path.lineTo(p2x, p2y); path.lineTo(p2x, p2y);
path.moveTo(p2x, p2y); path.moveTo(p2x, p2y);
path.closePath(); path.closePath();
return path;
}
AffineTransform transform = new AffineTransform(); @SuppressWarnings({ "rawtypes", "unchecked" })
final double deltaY = p2.getY() - originY; private static <V> Shape getVertexShapeForEdge(V v, Function<? super V, Shape> vertexShaper) {
final double deltaX = p2.getX() - originX; if (vertexShaper instanceof VisualGraphVertexShapeTransformer) {
if (deltaX == 0 && deltaY == 0) { if (v instanceof VisualVertex) {
// this implies the source and destination node are at the same location, which VisualVertex vv = (VisualVertex) v;
// is possible if the user drags it there or during animations
return transform.createTransformedShape(path); // 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);
}
} }
return vertexShaper.apply(v);
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;
} }
private void logMissingLocation(E e, V v) { private void logMissingLocation(E e, V v) {
@ -151,4 +155,5 @@ public class ArticulatedEdgeTransformer<V extends VisualVertex, E extends Visual
throw new IllegalStateException("Edge vertex is missing a location"); throw new IllegalStateException("Edge vertex is missing a location");
} }
} }
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -66,7 +66,6 @@ public class VisualGraphShapePickSupport<V extends VisualVertex, E extends Visua
if (edgeShape == null) { if (edgeShape == null) {
continue; continue;
} }
// because of the transform, the edgeShape is now a GeneralPath // because of the transform, the edgeShape is now a GeneralPath
// see if this edge is the closest of any that intersect // see if this edge is the closest of any that intersect
if (edgeShape.intersects(pickArea)) { if (edgeShape.intersects(pickArea)) {

View file

@ -4,16 +4,16 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package docking; package generic.theme;
import java.awt.*; import java.awt.*;
import java.awt.geom.GeneralPath; import java.awt.geom.GeneralPath;
@ -31,11 +31,10 @@ public class CloseIcon implements Icon {
/** /**
* Creates a close icon. * Creates a close icon.
* @param isSmall false signals to use a 16x16 size; true signals to use an 8x8 size * @param isSmall false signals to use a 16x16 size; true signals to use an 8x8 size
* @param color the color of the "x"
*/ */
public CloseIcon(boolean isSmall, Color color) { public CloseIcon(boolean isSmall) {
this.size = isSmall ? 8 : 16; this.size = isSmall ? 8 : 16;
this.color = color; this.color = new GColor("color.fg.button");
this.shape = buildShape(); this.shape = buildShape();
} }

Some files were not shown because too many files have changed in this diff Show more