GP-307 - Graph - Add ability to disable tootlips; better tooltip behavior

This commit is contained in:
dragonmacher 2020-10-29 16:00:30 -04:00
parent ba80c729ec
commit c5087c7ef0
8 changed files with 633 additions and 264 deletions

View file

@ -1091,8 +1091,9 @@ class FGActionManager {
AddressSet subtraction = provider.getCurrentProgramSelection().subtract(functionBody);
ProgramSelection programSelectionWithoutGraphBody = new ProgramSelection(subtraction);
plugin.getTool().firePluginEvent(new ProgramSelectionPluginEvent("Spoof!",
programSelectionWithoutGraphBody, provider.getCurrentProgram()));
plugin.getTool()
.firePluginEvent(new ProgramSelectionPluginEvent("Spoof!",
programSelectionWithoutGraphBody, provider.getCurrentProgram()));
}
private Set<FGVertex> getAllVertices() {
@ -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) {

View file

@ -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,24 +175,23 @@ 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 -> {
AttributedVertex source = g.getEdgeSource(e);
AttributedVertex target = g.getEdgeTarget(e);
attributedGraph.addEdge(source, target, e);
});
displaySubGraph(attributedGraph);
}
catch (CancelledException e) {
// noop
}
AttributedGraph attributedGraph = new AttributedGraph();
g.vertexSet().forEach(attributedGraph::addVertex);
g.edgeSet().forEach(e -> {
AttributedVertex source = g.getEdgeSource(e);
AttributedVertex target = g.getEdgeTarget(e);
attributedGraph.addEdge(source, target, e);
});
displaySubGraph(attributedGraph);
};
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,11 +634,17 @@ public class DefaultGraphDisplay implements GraphDisplay {
viewer.repaint();
}
private void displaySubGraph(Graph<AttributedVertex, AttributedEdge> subGraph)
throws CancelledException {
GraphDisplay graphDisplay = graphDisplayProvider.getGraphDisplay(false, TaskMonitor.DUMMY);
graphDisplay.setGraph((AttributedGraph) subGraph, "SubGraph", false, TaskMonitor.DUMMY);
graphDisplay.setGraphDisplayListener(listener);
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
}
}
/**
@ -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
}
}
}

View file

@ -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
//==================================================================================================
/*package*/ void setPopupDelay(int delayMs) {
popupRegulator.setPopupDelay(delayMs);
}
public void setPopupsVisible(boolean visible) {
this.showPopups = visible;
if (!showPopups) {
hidePopupTooltips();
}
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
}
@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();
}
});
@Override
public ToolTipInfo<?> getToolTipInfo(MouseEvent event) {
return viewer().getToolTipInfo(event);
}
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) {
@Override
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());
}
}
/** 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 addMouseMotionListener(MouseMotionListener l) {
viewer().addMouseMotionListener(l);
}
protected abstract JComponent createToolTipComponent(T t);
protected abstract void emphasize();
protected abstract void deEmphasize();
MouseEvent getMouseEvent() {
return event;
@Override
public void repaint() {
viewer().repaint();
}
JComponent getToolTipComponent() {
return tooltipComponent;
@Override
public Window getPopupParent() {
return WindowUtilities.windowForComponent(viewer());
}
}
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;
}

View file

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

View file

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

View file

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

View file

@ -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) {
StringBuilder buf = new StringBuilder("<html>");
for (Map.Entry<String, String> entry : entrySet()) {
buf.append(entry.getKey());
buf.append(":");
buf.append(entry.getValue());
buf.append("<br>");
}
htmlString = buf.toString();
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 : entries) {
buf.append(entry.getKey());
buf.append(":");
buf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
buf.append("<br>");
}
htmlString = buf.toString();
return htmlString;
}

View file

@ -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) {
StringBuilder buf = new StringBuilder("<html>");
for (Map.Entry<String, String> entry : entrySet()) {
buf.append(entry.getKey());
buf.append(":");
buf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
buf.append("<br>");
}
htmlString = buf.toString();
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 : entries) {
buf.append(entry.getKey());
buf.append(":");
buf.append(entry.getValue());
buf.append("<br>");
}
htmlString = buf.toString();
return htmlString;
}