mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 17:59:46 +02:00
GP-307 - Graph - Add ability to disable tootlips; better tooltip behavior
This commit is contained in:
parent
ba80c729ec
commit
c5087c7ef0
8 changed files with 633 additions and 264 deletions
|
@ -1091,7 +1091,8 @@ class FGActionManager {
|
|||
AddressSet subtraction = provider.getCurrentProgramSelection().subtract(functionBody);
|
||||
|
||||
ProgramSelection programSelectionWithoutGraphBody = new ProgramSelection(subtraction);
|
||||
plugin.getTool().firePluginEvent(new ProgramSelectionPluginEvent("Spoof!",
|
||||
plugin.getTool()
|
||||
.firePluginEvent(new ProgramSelectionPluginEvent("Spoof!",
|
||||
programSelectionWithoutGraphBody, provider.getCurrentProgram()));
|
||||
}
|
||||
|
||||
|
@ -1161,8 +1162,10 @@ class FGActionManager {
|
|||
|
||||
private void makeSelectionFromAddresses(AddressSet addresses) {
|
||||
ProgramSelection selection = new ProgramSelection(addresses);
|
||||
plugin.getTool().firePluginEvent(
|
||||
new ProgramSelectionPluginEvent("Spoof!", selection, provider.getCurrentProgram()));
|
||||
plugin.getTool()
|
||||
.firePluginEvent(
|
||||
new ProgramSelectionPluginEvent("Spoof!", selection,
|
||||
provider.getCurrentProgram()));
|
||||
}
|
||||
|
||||
private void ungroupVertices(Set<GroupedFunctionGraphVertex> groupVertices) {
|
||||
|
|
|
@ -32,6 +32,7 @@ import javax.swing.*;
|
|||
import javax.swing.event.AncestorEvent;
|
||||
import javax.swing.event.AncestorListener;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jgrapht.Graph;
|
||||
import org.jgrapht.graph.AsSubgraph;
|
||||
import org.jungrapht.visualization.*;
|
||||
|
@ -58,9 +59,11 @@ import docking.action.ToggleDockingAction;
|
|||
import docking.action.builder.*;
|
||||
import docking.menu.ActionState;
|
||||
import docking.widgets.EventTrigger;
|
||||
import generic.util.WindowUtilities;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.graph.AttributeFilters;
|
||||
import ghidra.graph.job.GraphJobRunner;
|
||||
import ghidra.graph.viewer.popup.*;
|
||||
import ghidra.service.graph.*;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
|
@ -172,7 +175,7 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
* a new tab or new window
|
||||
*/
|
||||
Consumer<Graph<AttributedVertex, AttributedEdge>> subgraphConsumer = g -> {
|
||||
try {
|
||||
|
||||
AttributedGraph attributedGraph = new AttributedGraph();
|
||||
g.vertexSet().forEach(attributedGraph::addVertex);
|
||||
g.edgeSet().forEach(e -> {
|
||||
|
@ -181,15 +184,14 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
attributedGraph.addEdge(source, target, e);
|
||||
});
|
||||
displaySubGraph(attributedGraph);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
private ToggleDockingAction hideSelectedAction;
|
||||
private ToggleDockingAction hideUnselectedAction;
|
||||
private SwitchableSelectionItemListener switchableSelectionListener;
|
||||
|
||||
private ToggleDockingAction togglePopupsAction;
|
||||
private PopupRegulator<AttributedVertex, AttributedEdge> popupRegulator;
|
||||
|
||||
/**
|
||||
* Create the initial display, the graph-less visualization viewer, and its controls
|
||||
* @param displayProvider provides a {@link PluginTool} for Docking features
|
||||
|
@ -470,6 +472,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
.enabledWhen(c -> !viewer.getSelectedVertexState().getSelected().isEmpty())
|
||||
.onAction(c -> createAndDisplaySubGraph())
|
||||
.buildAndInstallLocal(componentProvider);
|
||||
|
||||
togglePopupsAction = new ToggleActionBuilder("Display Popup Windows", actionOwnerName)
|
||||
.popupMenuPath("Display Popup Windows")
|
||||
.popupMenuGroup("zz", "1")
|
||||
.description("Toggles whether or not to show popup windows, such as tool tips")
|
||||
.selected(true)
|
||||
.onAction(c -> popupRegulator.setPopupsVisible(togglePopupsAction.isSelected()))
|
||||
.buildAndInstallLocal(componentProvider);
|
||||
popupRegulator.setPopupsVisible(togglePopupsAction.isSelected());
|
||||
|
||||
}
|
||||
|
||||
private void createAndDisplaySubGraph() {
|
||||
|
@ -622,12 +634,18 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
viewer.repaint();
|
||||
}
|
||||
|
||||
private void displaySubGraph(Graph<AttributedVertex, AttributedEdge> subGraph)
|
||||
throws CancelledException {
|
||||
GraphDisplay graphDisplay = graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
|
||||
private void displaySubGraph(Graph<AttributedVertex, AttributedEdge> subGraph) {
|
||||
|
||||
try {
|
||||
GraphDisplay graphDisplay =
|
||||
graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
|
||||
graphDisplay.setGraph((AttributedGraph) subGraph, "SubGraph", false, TaskMonitor.DUMMY);
|
||||
graphDisplay.setGraphDisplayListener(listener);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// can't happen while using a dummy monitor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a SatelliteViewer for the Visualization
|
||||
|
@ -746,15 +764,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* transform the supplied {@code AttributedVertex}s to a List of their ids
|
||||
* @param selectedVertices the collections of vertices.
|
||||
* @return a list of vertex ids
|
||||
*/
|
||||
private List<String> toVertexIds(Collection<AttributedVertex> selectedVertices) {
|
||||
return selectedVertices.stream().map(AttributedVertex::getId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Collection<AttributedVertex> getVertices(Object item) {
|
||||
if (item instanceof Collection) {
|
||||
|
@ -807,7 +816,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* set the {@link AttributedGraph} for visualization
|
||||
* @param attributedGraph the {@link AttributedGraph} to visualize
|
||||
|
@ -1097,9 +1105,16 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
}
|
||||
});
|
||||
|
||||
// We control tooltips with the PopupRegulator. Use null values to disable the default
|
||||
// tool tip mechanism
|
||||
vv.setVertexToolTipFunction(v -> null);
|
||||
vv.setEdgeToolTipFunction(e -> null);
|
||||
vv.setToolTipText(null);
|
||||
|
||||
PopupSource<AttributedVertex, AttributedEdge> popupSource = new GraphDisplayPopupSource(vv);
|
||||
popupRegulator = new PopupRegulator<>(popupSource);
|
||||
|
||||
this.iconCache = new GhidraIconCache();
|
||||
vv.setVertexToolTipFunction(AttributedVertex::getHtmlString);
|
||||
vv.setEdgeToolTipFunction(AttributedEdge::getHtmlString);
|
||||
RenderContext<AttributedVertex, AttributedEdge> renderContext = vv.getRenderContext();
|
||||
|
||||
// set up the shape and color functions
|
||||
|
@ -1138,7 +1153,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
renderContext.setArrowFillPaintFunction(
|
||||
e -> renderContext.getSelectedEdgeState().isSelected(e) ? Color.red
|
||||
: Colors.getColor(e));
|
||||
vv.setToolTipText("");
|
||||
|
||||
// assign the shapes to the modal renderer
|
||||
ModalRenderer<AttributedVertex, AttributedEdge> modalRenderer = vv.getRenderer();
|
||||
|
@ -1248,7 +1262,6 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use the hide selected action states to determine what vertices are shown:
|
||||
* <ul>
|
||||
|
@ -1301,4 +1314,135 @@ public class DefaultGraphDisplay implements GraphDisplay {
|
|||
});
|
||||
}
|
||||
|
||||
// class passed to the PopupRegulator to help construct info popups for the graph
|
||||
private class GraphDisplayPopupSource implements PopupSource<AttributedVertex, AttributedEdge> {
|
||||
|
||||
private VisualizationViewer<AttributedVertex, AttributedEdge> vv;
|
||||
|
||||
public GraphDisplayPopupSource(VisualizationViewer<AttributedVertex, AttributedEdge> vv) {
|
||||
this.vv = vv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolTipInfo<?> getToolTipInfo(MouseEvent event) {
|
||||
|
||||
// check for a vertex hit first, otherwise, we get edge hits when we are hovering
|
||||
// over a vertex, due to how edges are interpreted as existing all the way to the
|
||||
// center point of a vertex
|
||||
AttributedVertex vertex = getVertex(event);
|
||||
if (vertex != null) {
|
||||
return new VertexToolTipInfo(vertex, event);
|
||||
}
|
||||
|
||||
AttributedEdge edge = getEdge(event);
|
||||
if (edge != null) {
|
||||
return new EdgeToolTipInfo(edge, event);
|
||||
}
|
||||
|
||||
// no vertex or edge hit; just create a basic info that is essentially a null-object
|
||||
// placeholder to prevent NPEs
|
||||
return new VertexToolTipInfo(vertex, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedVertex getVertex(MouseEvent event) {
|
||||
|
||||
LayoutModel<AttributedVertex> layoutModel =
|
||||
vv.getVisualizationModel().getLayoutModel();
|
||||
Point2D p = vv.getTransformSupport().inverseTransform(vv, event.getPoint());
|
||||
AttributedVertex vertex =
|
||||
vv.getPickSupport().getVertex(layoutModel, p.getX(), p.getY());
|
||||
return vertex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedEdge getEdge(MouseEvent event) {
|
||||
LayoutModel<AttributedVertex> layoutModel =
|
||||
vv.getVisualizationModel().getLayoutModel();
|
||||
Point2D p = vv.getTransformSupport().inverseTransform(vv, event.getPoint());
|
||||
AttributedEdge edge = vv.getPickSupport().getEdge(layoutModel, p.getX(), p.getY());
|
||||
return edge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMouseMotionListener(MouseMotionListener l) {
|
||||
vv.getComponent().addMouseMotionListener(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repaint() {
|
||||
vv.repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getPopupParent() {
|
||||
return WindowUtilities.windowForComponent(vv.getComponent());
|
||||
}
|
||||
}
|
||||
|
||||
private class VertexToolTipInfo extends ToolTipInfo<AttributedVertex> {
|
||||
|
||||
VertexToolTipInfo(AttributedVertex vertex, MouseEvent event) {
|
||||
super(event, vertex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent createToolTipComponent() {
|
||||
if (graphObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String toolTip = graphObject.getHtmlString();
|
||||
if (StringUtils.isBlank(toolTip)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JToolTip jToolTip = new JToolTip();
|
||||
jToolTip.setTipText(toolTip);
|
||||
return jToolTip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void emphasize() {
|
||||
// this graph display does not have a notion of emphasizing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deEmphasize() {
|
||||
// this graph display does not have a notion of emphasizing
|
||||
}
|
||||
}
|
||||
|
||||
private class EdgeToolTipInfo extends ToolTipInfo<AttributedEdge> {
|
||||
|
||||
EdgeToolTipInfo(AttributedEdge edge, MouseEvent event) {
|
||||
super(event, edge);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JComponent createToolTipComponent() {
|
||||
if (graphObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String toolTip = graphObject.getHtmlString();
|
||||
if (StringUtils.isBlank(toolTip)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JToolTip jToolTip = new JToolTip();
|
||||
jToolTip.setTipText(toolTip);
|
||||
return jToolTip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void emphasize() {
|
||||
// this graph display does not have a notion of emphasizing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deEmphasize() {
|
||||
// this graph display does not have a notion of emphasizing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
package ghidra.graph.viewer;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import docking.widgets.PopupWindow;
|
||||
import edu.uci.ics.jung.algorithms.layout.Layout;
|
||||
import edu.uci.ics.jung.visualization.VisualizationViewer;
|
||||
import edu.uci.ics.jung.visualization.picking.MultiPickedState;
|
||||
|
@ -35,6 +35,7 @@ import ghidra.graph.viewer.event.mouse.*;
|
|||
import ghidra.graph.viewer.event.picking.GPickedState;
|
||||
import ghidra.graph.viewer.layout.VisualGraphLayout;
|
||||
import ghidra.graph.viewer.options.VisualGraphOptions;
|
||||
import ghidra.graph.viewer.popup.*;
|
||||
import ghidra.graph.viewer.renderer.VisualGraphRenderer;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
|
||||
|
@ -62,9 +63,7 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
|
||||
private Consumer<GraphViewer<V, E>> initializedListener;
|
||||
|
||||
private PopupRegulator popupRegulator = new PopupRegulator();
|
||||
private PopupWindow popupWindow;
|
||||
private boolean showPopups = true;
|
||||
private PopupRegulator<V, E> popupRegulator;
|
||||
private VertexTooltipProvider<V, E> vertexTooltipProvider = new DummyTooltipProvider();
|
||||
|
||||
protected VisualGraphOptions options;
|
||||
|
@ -88,6 +87,8 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
PickedState<V> pickedState = getPickedVertexState();
|
||||
gPickedState = new GPickedState<>((MultiPickedState<V>) pickedState);
|
||||
setPickedVertexState(gPickedState);
|
||||
|
||||
popupRegulator = new PopupRegulator<V, E>(new GraphViewerPopupSource());
|
||||
}
|
||||
|
||||
private void buildUpdater() {
|
||||
|
@ -271,23 +272,16 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
// Popups and Tooltips
|
||||
//==================================================================================================
|
||||
|
||||
public void setPopupsVisible(boolean visible) {
|
||||
this.showPopups = visible;
|
||||
if (!showPopups) {
|
||||
hidePopupTooltips();
|
||||
/*package*/ void setPopupDelay(int delayMs) {
|
||||
popupRegulator.setPopupDelay(delayMs);
|
||||
}
|
||||
|
||||
public void setPopupsVisible(boolean visible) {
|
||||
popupRegulator.setPopupsVisible(visible);
|
||||
}
|
||||
|
||||
/*package*/ boolean isPopupShowing() {
|
||||
return popupWindow != null && popupWindow.isShowing();
|
||||
}
|
||||
|
||||
private void hidePopupTooltips() {
|
||||
if (popupWindow != null && popupWindow.isShowing()) {
|
||||
popupWindow.hide();
|
||||
// don't call dispose, or we don't get our componentHidden() callback
|
||||
// popupWindow.dispose();
|
||||
}
|
||||
return popupRegulator.isPopupShowing();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -317,49 +311,11 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
return new VertexToolTipInfo(vertex, event);
|
||||
}
|
||||
|
||||
private void showTooltip(ToolTipInfo<?> info) {
|
||||
JComponent tipComponent = info.getToolTipComponent();
|
||||
if (tipComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MouseEvent event = info.getMouseEvent();
|
||||
showPopupWindow(event, tipComponent);
|
||||
}
|
||||
|
||||
private void showPopupWindow(MouseEvent event, JComponent component) {
|
||||
MenuSelectionManager menuManager = MenuSelectionManager.defaultManager();
|
||||
if (menuManager.getSelectedPath().length != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Window parentWindow = WindowUtilities.windowForComponent(this);
|
||||
popupWindow = new PopupWindow(parentWindow, component);
|
||||
|
||||
popupWindow.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
popupRegulator.popupShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentHidden(ComponentEvent e) {
|
||||
popupRegulator.popupHidden();
|
||||
}
|
||||
});
|
||||
|
||||
popupWindow.showPopup(event);
|
||||
}
|
||||
|
||||
public VertexMouseInfo<V, E> createVertexMouseInfo(MouseEvent e, V v,
|
||||
Point2D vertexBasedClickPoint) {
|
||||
return new VertexMouseInfo<>(e, v, vertexBasedClickPoint, this);
|
||||
}
|
||||
|
||||
/*package*/ void setPopupDelay(int delayMs) {
|
||||
popupRegulator.setPopupDelay(delayMs);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
||||
viewUpdater.dispose();
|
||||
|
@ -369,168 +325,50 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
removeAll();
|
||||
}
|
||||
|
||||
private GraphViewer<V, E> viewer() {
|
||||
return GraphViewer.this;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class PopupRegulator {
|
||||
private int popupDelay = 1000;
|
||||
private class GraphViewerPopupSource implements PopupSource<V, E> {
|
||||
|
||||
/**
|
||||
* We need this timer because the default mechanism for triggering popups doesn't
|
||||
* always work. We use this timer in conjunction with a mouse motion listener to
|
||||
* get the results we want.
|
||||
*/
|
||||
private Timer popupTimer;
|
||||
private MouseEvent popupMouseEvent;
|
||||
|
||||
/** the current target (vertex or edge) of a popup window */
|
||||
private Object nextPopupTarget;
|
||||
|
||||
/**
|
||||
* This value is not null when the user moves the cursor over a target for which a
|
||||
* popup is already showing. We use this value to prevent showing a popup multiple times
|
||||
* while over a single node.
|
||||
*/
|
||||
private Object lastShownPopupTarget;
|
||||
|
||||
/** The tooltip info used when showing the popup */
|
||||
private ToolTipInfo<?> currentToolTipInfo;
|
||||
|
||||
PopupRegulator() {
|
||||
popupTimer = new Timer(popupDelay, e -> {
|
||||
if (isPopupShowing()) {
|
||||
return; // don't show any new popups while the user is perusing
|
||||
}
|
||||
showPopupForMouseEvent(popupMouseEvent);
|
||||
});
|
||||
|
||||
popupTimer.setRepeats(false);
|
||||
|
||||
addMouseMotionListener(new MouseMotionListener() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
hidePopupTooltips();
|
||||
popupTimer.stop();
|
||||
popupMouseEvent = null; // clear any queued popups
|
||||
public ToolTipInfo<?> getToolTipInfo(MouseEvent event) {
|
||||
return viewer().getToolTipInfo(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
popupMouseEvent = e;
|
||||
|
||||
// this clears out the current last popup shown so that the user can
|
||||
// move off and on a node to re-show the popup
|
||||
savePopupTarget(e);
|
||||
|
||||
// make sure the popup gets triggered eventually
|
||||
popupTimer.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setPopupDelay(int delayMs) {
|
||||
popupTimer.stop();
|
||||
popupTimer.setDelay(delayMs);
|
||||
popupTimer.setInitialDelay(delayMs);
|
||||
popupDelay = delayMs;
|
||||
}
|
||||
|
||||
private void showPopupForMouseEvent(MouseEvent event) {
|
||||
if (!showPopups) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ToolTipInfo<?> toolTipInfo = getToolTipInfo(event);
|
||||
JComponent toolTipComponent = toolTipInfo.getToolTipComponent();
|
||||
boolean isCustomJavaTooltip = !(toolTipComponent instanceof JToolTip);
|
||||
if (lastShownPopupTarget == nextPopupTarget && isCustomJavaTooltip) {
|
||||
//
|
||||
// Kinda Hacky:
|
||||
// We don't show repeated popups for the same item (the user has to move away
|
||||
// and then come back to re-show the popup). However, one caveat to this is that
|
||||
// we do want to allow the user to see popups for the toolbar actions always. So,
|
||||
// only return here if we have already shown a popup for the item *and* we are
|
||||
// using a custom tooltip (which is used to show a vertex tooltip or an edge
|
||||
// tooltip)
|
||||
return;
|
||||
}
|
||||
|
||||
currentToolTipInfo = toolTipInfo;
|
||||
showTooltip(currentToolTipInfo);
|
||||
}
|
||||
|
||||
void popupShown() {
|
||||
lastShownPopupTarget = nextPopupTarget;
|
||||
currentToolTipInfo.emphasize();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void popupHidden() {
|
||||
currentToolTipInfo.deEmphasize();
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void savePopupTarget(MouseEvent event) {
|
||||
nextPopupTarget = null;
|
||||
V vertex = getVertexForEvent(event);
|
||||
if (vertex != null) {
|
||||
nextPopupTarget = vertex;
|
||||
}
|
||||
else {
|
||||
E edge = getEdgeForEvent(event);
|
||||
nextPopupTarget = edge;
|
||||
}
|
||||
|
||||
if (nextPopupTarget == null) {
|
||||
// We've moved off of a target. We will clear that last target so the user can
|
||||
// mouse off of a vertex and back on in order to trigger a new popup
|
||||
lastShownPopupTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
private V getVertexForEvent(MouseEvent event) {
|
||||
public V getVertex(MouseEvent event) {
|
||||
Layout<V, E> viewerLayout = getGraphLayout();
|
||||
Point p = event.getPoint();
|
||||
return getPickSupport().getVertex(viewerLayout, p.getX(), p.getY());
|
||||
}
|
||||
|
||||
private E getEdgeForEvent(MouseEvent event) {
|
||||
@Override
|
||||
public E getEdge(MouseEvent event) {
|
||||
Layout<V, E> viewerLayout = getGraphLayout();
|
||||
Point p = event.getPoint();
|
||||
return getPickSupport().getEdge(viewerLayout, p.getX(), p.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMouseMotionListener(MouseMotionListener l) {
|
||||
viewer().addMouseMotionListener(l);
|
||||
}
|
||||
|
||||
/** Basic container object that knows how to generate tooltips */
|
||||
private abstract class ToolTipInfo<T> {
|
||||
protected final MouseEvent event;
|
||||
protected final T graphObject;
|
||||
private JComponent tooltipComponent;
|
||||
|
||||
ToolTipInfo(MouseEvent event, T t) {
|
||||
this.event = event;
|
||||
this.graphObject = t;
|
||||
tooltipComponent = createToolTipComponent(t);
|
||||
@Override
|
||||
public void repaint() {
|
||||
viewer().repaint();
|
||||
}
|
||||
|
||||
protected abstract JComponent createToolTipComponent(T t);
|
||||
|
||||
protected abstract void emphasize();
|
||||
|
||||
protected abstract void deEmphasize();
|
||||
|
||||
MouseEvent getMouseEvent() {
|
||||
return event;
|
||||
@Override
|
||||
public Window getPopupParent() {
|
||||
return WindowUtilities.windowForComponent(viewer());
|
||||
}
|
||||
|
||||
JComponent getToolTipComponent() {
|
||||
return tooltipComponent;
|
||||
}
|
||||
}
|
||||
|
||||
private class VertexToolTipInfo extends ToolTipInfo<V> {
|
||||
|
@ -540,19 +378,20 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
}
|
||||
|
||||
@Override
|
||||
public JComponent createToolTipComponent(V vertex) {
|
||||
if (vertex == null) {
|
||||
public JComponent createToolTipComponent() {
|
||||
if (graphObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isScaledPastInteractionThreshold()) {
|
||||
return vertexTooltipProvider.getTooltip(vertex);
|
||||
return vertexTooltipProvider.getTooltip(graphObject);
|
||||
}
|
||||
|
||||
VertexMouseInfo<V, E> mouseInfo =
|
||||
GraphViewerUtils.convertMouseEventToVertexMouseEvent(GraphViewer.this, event);
|
||||
MouseEvent translatedMouseEvent = mouseInfo.getTranslatedMouseEvent();
|
||||
String toolTip = vertexTooltipProvider.getTooltipText(vertex, translatedMouseEvent);
|
||||
String toolTip =
|
||||
vertexTooltipProvider.getTooltipText(graphObject, translatedMouseEvent);
|
||||
if (toolTip == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -591,15 +430,15 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
}
|
||||
|
||||
@Override
|
||||
public JComponent createToolTipComponent(E edge) {
|
||||
if (edge == null) {
|
||||
public JComponent createToolTipComponent() {
|
||||
if (graphObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
V start = edge.getStart();
|
||||
V end = edge.getEnd();
|
||||
V start = graphObject.getStart();
|
||||
V end = graphObject.getEnd();
|
||||
|
||||
JComponent startComponent = vertexTooltipProvider.getTooltip(start, edge);
|
||||
JComponent startComponent = vertexTooltipProvider.getTooltip(start, graphObject);
|
||||
if (startComponent == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -611,7 +450,7 @@ public class GraphViewer<V extends VisualVertex, E extends VisualEdge<V>>
|
|||
return component;
|
||||
}
|
||||
|
||||
JComponent endComponent = vertexTooltipProvider.getTooltip(end, edge);
|
||||
JComponent endComponent = vertexTooltipProvider.getTooltip(end, graphObject);
|
||||
if (endComponent == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.graph.viewer.popup;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import docking.widgets.PopupWindow;
|
||||
|
||||
/**
|
||||
* A class to control popups for graph clients, bypassing Java's default tool tip mechanism
|
||||
*
|
||||
* @param <V> the vertex type
|
||||
* @param <E> the edge type
|
||||
*/
|
||||
public class PopupRegulator<V, E> {
|
||||
|
||||
private int popupDelay = 1000;
|
||||
|
||||
/**
|
||||
* We need this timer because the default mechanism for triggering popups doesn't
|
||||
* always work. We use this timer in conjunction with a mouse motion listener to
|
||||
* get the results we want.
|
||||
*/
|
||||
private Timer popupTimer;
|
||||
private MouseEvent popupMouseEvent;
|
||||
|
||||
/** the current target (vertex or edge) of a popup window */
|
||||
private Object nextPopupTarget;
|
||||
|
||||
/**
|
||||
* This value is not null when the user moves the cursor over a target for which a
|
||||
* popup is already showing. We use this value to prevent showing a popup multiple times
|
||||
* while over a single node.
|
||||
*/
|
||||
private Object lastShownPopupTarget;
|
||||
|
||||
/** The tooltip info used when showing the popup */
|
||||
private ToolTipInfo<?> currentToolTipInfo;
|
||||
|
||||
private PopupSource<V, E> popupSource;
|
||||
private PopupWindow popupWindow;
|
||||
private boolean showPopups = true;
|
||||
|
||||
public PopupRegulator(PopupSource<V, E> popupSupplier) {
|
||||
this.popupSource = popupSupplier;
|
||||
popupTimer = new Timer(popupDelay, e -> {
|
||||
if (isPopupShowing()) {
|
||||
return; // don't show any new popups while the user is perusing
|
||||
}
|
||||
showPopupForMouseEvent(popupMouseEvent);
|
||||
});
|
||||
|
||||
popupTimer.setRepeats(false);
|
||||
|
||||
popupSupplier.addMouseMotionListener(new MouseMotionListener() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
hidePopupTooltips();
|
||||
popupTimer.stop();
|
||||
popupMouseEvent = null; // clear any queued popups
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
popupMouseEvent = e;
|
||||
|
||||
// this clears out the current last popup shown so that the user can
|
||||
// move off and on a node to re-show the popup
|
||||
savePopupTarget(e);
|
||||
|
||||
// make sure the popup gets triggered eventually
|
||||
popupTimer.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this class's popup is showing
|
||||
* @return true if this class's popup is showing
|
||||
*/
|
||||
public boolean isPopupShowing() {
|
||||
return popupWindow != null && popupWindow.isShowing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time between mouse movements to wait before showing this class's popup
|
||||
* @param delayMs the delay
|
||||
*/
|
||||
public void setPopupDelay(int delayMs) {
|
||||
popupTimer.stop();
|
||||
popupTimer.setDelay(delayMs);
|
||||
popupTimer.setInitialDelay(delayMs);
|
||||
popupDelay = delayMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enablement of this class's popup
|
||||
* @param visible true to have popups enabled
|
||||
*/
|
||||
public void setPopupsVisible(boolean visible) {
|
||||
this.showPopups = visible;
|
||||
if (!showPopups) {
|
||||
hidePopupTooltips();
|
||||
}
|
||||
}
|
||||
|
||||
private void showPopupForMouseEvent(MouseEvent event) {
|
||||
if (!showPopups) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ToolTipInfo<?> toolTipInfo = popupSource.getToolTipInfo(event);
|
||||
JComponent toolTipComponent = toolTipInfo.getToolTipComponent();
|
||||
boolean isCustomJavaTooltip = !(toolTipComponent instanceof JToolTip);
|
||||
if (lastShownPopupTarget == nextPopupTarget && isCustomJavaTooltip) {
|
||||
//
|
||||
// Kinda Hacky:
|
||||
// We don't show repeated popups for the same item (the user has to move away
|
||||
// and then come back to re-show the popup). However, one caveat to this is that
|
||||
// we do want to allow the user to see popups for the toolbar actions always. So,
|
||||
// only return here if we have already shown a popup for the item *and* we are
|
||||
// using a custom tooltip (which is used to show a vertex tooltip or an edge
|
||||
// tooltip)
|
||||
return;
|
||||
}
|
||||
|
||||
currentToolTipInfo = toolTipInfo;
|
||||
showTooltip(currentToolTipInfo);
|
||||
}
|
||||
|
||||
private void popupShown() {
|
||||
lastShownPopupTarget = nextPopupTarget;
|
||||
currentToolTipInfo.emphasize();
|
||||
popupSource.repaint();
|
||||
}
|
||||
|
||||
private void popupHidden() {
|
||||
currentToolTipInfo.deEmphasize();
|
||||
popupSource.repaint();
|
||||
}
|
||||
|
||||
private void savePopupTarget(MouseEvent event) {
|
||||
nextPopupTarget = null;
|
||||
V vertex = popupSource.getVertex(event);
|
||||
if (vertex != null) {
|
||||
nextPopupTarget = vertex;
|
||||
}
|
||||
else {
|
||||
E edge = popupSource.getEdge(event);
|
||||
nextPopupTarget = edge;
|
||||
}
|
||||
|
||||
if (nextPopupTarget == null) {
|
||||
// We've moved off of a target. We will clear that last target so the user can
|
||||
// mouse off of a vertex and back on in order to trigger a new popup
|
||||
lastShownPopupTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void hidePopupTooltips() {
|
||||
if (popupWindow != null && popupWindow.isShowing()) {
|
||||
popupWindow.hide();
|
||||
// don't call dispose, or we don't get our componentHidden() callback
|
||||
// popupWindow.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void showTooltip(ToolTipInfo<?> info) {
|
||||
JComponent tipComponent = info.getToolTipComponent();
|
||||
if (tipComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MouseEvent event = info.getMouseEvent();
|
||||
showPopupWindow(event, tipComponent);
|
||||
}
|
||||
|
||||
private void showPopupWindow(MouseEvent event, JComponent component) {
|
||||
MenuSelectionManager menuManager = MenuSelectionManager.defaultManager();
|
||||
if (menuManager.getSelectedPath().length != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Window parentWindow = popupSource.getPopupParent();
|
||||
popupWindow = new PopupWindow(parentWindow, component);
|
||||
|
||||
popupWindow.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
popupShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentHidden(ComponentEvent e) {
|
||||
popupHidden();
|
||||
}
|
||||
});
|
||||
|
||||
popupWindow.showPopup(event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.graph.viewer.popup;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
|
||||
/**
|
||||
* An interface that provides graph and component information to the {@link PopupRegulator}
|
||||
*
|
||||
* @param <V> the vertex type
|
||||
* @param <E> the edge type
|
||||
*/
|
||||
public interface PopupSource<V, E> {
|
||||
|
||||
/**
|
||||
* Returns the tool tip info object for the given mouse event. Implementations will use the
|
||||
* event to determine whether a popup should be created for a vertex, edge, the graph or
|
||||
* not at all.
|
||||
*
|
||||
* @param event the event
|
||||
* @return the info; null for no popup
|
||||
*/
|
||||
public ToolTipInfo<?> getToolTipInfo(MouseEvent event);
|
||||
|
||||
/**
|
||||
* Returns a vertex for the given event
|
||||
* @param event the event
|
||||
* @return the vertex or null
|
||||
*/
|
||||
public V getVertex(MouseEvent event);
|
||||
|
||||
/**
|
||||
* Returns an edge for the given event
|
||||
* @param event the event
|
||||
* @return the edge or null
|
||||
*/
|
||||
public E getEdge(MouseEvent event);
|
||||
|
||||
/**
|
||||
* Adds the given mouse motion listener to the graph component. This allows the popup
|
||||
* regulator to decided when to show and hide popups.
|
||||
*
|
||||
* @param l the listener
|
||||
*/
|
||||
public void addMouseMotionListener(MouseMotionListener l);
|
||||
|
||||
/**
|
||||
* Signals that the graph needs to repaint
|
||||
*/
|
||||
public void repaint();
|
||||
|
||||
/**
|
||||
* Returns a suitable window parent for the popup window
|
||||
* @return the window parent
|
||||
*/
|
||||
public Window getPopupParent();
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.graph.viewer.popup;
|
||||
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
/**
|
||||
* Basic container object that knows how to generate tooltips
|
||||
*
|
||||
* @param <T> the type of object for which to create a tooltip
|
||||
*/
|
||||
public abstract class ToolTipInfo<T> {
|
||||
|
||||
protected final MouseEvent event;
|
||||
protected final T graphObject;
|
||||
private JComponent tooltipComponent;
|
||||
|
||||
public ToolTipInfo(MouseEvent event, T t) {
|
||||
this.event = event;
|
||||
this.graphObject = t;
|
||||
tooltipComponent = createToolTipComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tool tip component
|
||||
* @return the tool tip component
|
||||
*/
|
||||
protected abstract JComponent createToolTipComponent();
|
||||
|
||||
/**
|
||||
* Signals for the implementation to emphasis the original graph object passed to this info
|
||||
*/
|
||||
protected abstract void emphasize();
|
||||
|
||||
/**
|
||||
* Signals for the implementation to turn off emphasis
|
||||
*/
|
||||
protected abstract void deEmphasize();
|
||||
|
||||
/**
|
||||
* Returns the mouse event from this tool tip info
|
||||
* @return the mouse event from this tool tip info
|
||||
*/
|
||||
MouseEvent getMouseEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tool tip component created by this info
|
||||
* @return the tool tip component created by this info
|
||||
*/
|
||||
JComponent getToolTipComponent() {
|
||||
return tooltipComponent;
|
||||
}
|
||||
}
|
|
@ -16,14 +16,19 @@
|
|||
package ghidra.service.graph;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
/**
|
||||
* Generic directed graph edge implementation
|
||||
*/
|
||||
public class AttributedEdge extends Attributed {
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* cache of the edge label parsed as html
|
||||
* Cache of the edge label parsed as html
|
||||
*/
|
||||
private String htmlString;
|
||||
|
||||
|
@ -41,20 +46,27 @@ public class AttributedEdge extends Attributed {
|
|||
}
|
||||
|
||||
/**
|
||||
* create (once) the html representation of the key/values for this edge
|
||||
* The html representation of the key/values for this edge
|
||||
* @return html formatted label for the edge
|
||||
*/
|
||||
public String getHtmlString() {
|
||||
if (htmlString == null) {
|
||||
if (htmlString != null) {
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
Set<Entry<String, String>> entries = entrySet();
|
||||
if (entries.isEmpty()) {
|
||||
return ""; // empty so tooltip clients can handle empty data
|
||||
}
|
||||
|
||||
StringBuilder buf = new StringBuilder("<html>");
|
||||
for (Map.Entry<String, String> entry : entrySet()) {
|
||||
for (Map.Entry<String, String> entry : entries) {
|
||||
buf.append(entry.getKey());
|
||||
buf.append(":");
|
||||
buf.append(entry.getValue());
|
||||
buf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
|
||||
buf.append("<br>");
|
||||
}
|
||||
htmlString = buf.toString();
|
||||
}
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
*/
|
||||
package ghidra.service.graph;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Graph vertex with attributes
|
||||
|
@ -85,16 +85,24 @@ public class AttributedVertex extends Attributed {
|
|||
* @return the html string
|
||||
*/
|
||||
public String getHtmlString() {
|
||||
if (htmlString == null) {
|
||||
|
||||
if (htmlString != null) {
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
Set<Entry<String, String>> entries = entrySet();
|
||||
if (entries.isEmpty()) {
|
||||
return ""; // empty so tooltip clients can handle empty data
|
||||
}
|
||||
|
||||
StringBuilder buf = new StringBuilder("<html>");
|
||||
for (Map.Entry<String, String> entry : entrySet()) {
|
||||
for (Map.Entry<String, String> entry : entries) {
|
||||
buf.append(entry.getKey());
|
||||
buf.append(":");
|
||||
buf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
|
||||
buf.append(entry.getValue());
|
||||
buf.append("<br>");
|
||||
}
|
||||
htmlString = buf.toString();
|
||||
}
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue