diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java index 04a7c3c209..f6b77414f3 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGActionManager.java @@ -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 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 groupVertices) { diff --git a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java index 2e40b08456..ada833d126 100644 --- a/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java +++ b/Ghidra/Features/GraphServices/src/main/java/ghidra/graph/visualization/DefaultGraphDisplay.java @@ -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> 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 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 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 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 toVertexIds(Collection selectedVertices) { - return selectedVertices.stream().map(AttributedVertex::getId).collect(Collectors.toList()); - } - @SuppressWarnings("unchecked") private Collection 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 popupSource = new GraphDisplayPopupSource(vv); + popupRegulator = new PopupRegulator<>(popupSource); + this.iconCache = new GhidraIconCache(); - vv.setVertexToolTipFunction(AttributedVertex::getHtmlString); - vv.setEdgeToolTipFunction(AttributedEdge::getHtmlString); RenderContext 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 modalRenderer = vv.getRenderer(); @@ -1248,7 +1262,6 @@ public class DefaultGraphDisplay implements GraphDisplay { } - /** * Use the hide selected action states to determine what vertices are shown: *
    @@ -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 { + + private VisualizationViewer vv; + + public GraphDisplayPopupSource(VisualizationViewer 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 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 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 { + + 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 { + + 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 + } + } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewer.java index c0476bff40..98d9905e67 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphViewer.java @@ -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> private Consumer> initializedListener; - private PopupRegulator popupRegulator = new PopupRegulator(); - private PopupWindow popupWindow; - private boolean showPopups = true; + private PopupRegulator popupRegulator; private VertexTooltipProvider vertexTooltipProvider = new DummyTooltipProvider(); protected VisualGraphOptions options; @@ -88,6 +87,8 @@ public class GraphViewer> PickedState pickedState = getPickedVertexState(); gPickedState = new GPickedState<>((MultiPickedState) pickedState); setPickedVertexState(gPickedState); + + popupRegulator = new PopupRegulator(new GraphViewerPopupSource()); } private void buildUpdater() { @@ -271,23 +272,16 @@ public class GraphViewer> // 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> 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 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> removeAll(); } + private GraphViewer viewer() { + return GraphViewer.this; + } + //================================================================================================== // Inner Classes //================================================================================================== - private class PopupRegulator { - private int popupDelay = 1000; + private class GraphViewerPopupSource implements PopupSource { - /** - * 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 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 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 { - 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 { @@ -540,19 +378,20 @@ public class GraphViewer> } @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 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> } @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> return component; } - JComponent endComponent = vertexTooltipProvider.getTooltip(end, edge); + JComponent endComponent = vertexTooltipProvider.getTooltip(end, graphObject); if (endComponent == null) { return null; } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupRegulator.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupRegulator.java new file mode 100644 index 0000000000..50445c0f64 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupRegulator.java @@ -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 the vertex type + * @param the edge type + */ +public class PopupRegulator { + + 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 popupSource; + private PopupWindow popupWindow; + private boolean showPopups = true; + + public PopupRegulator(PopupSource 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); + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupSource.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupSource.java new file mode 100644 index 0000000000..7629a77d80 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/PopupSource.java @@ -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 the vertex type + * @param the edge type + */ +public interface PopupSource { + + /** + * 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(); +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/ToolTipInfo.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/ToolTipInfo.java new file mode 100644 index 0000000000..2e87e04505 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/popup/ToolTipInfo.java @@ -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 the type of object for which to create a tooltip + */ +public abstract class ToolTipInfo { + + 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; + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java index cd4f65b312..2c34b2b8a2 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedEdge.java @@ -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(""); - for (Map.Entry entry : entrySet()) { - buf.append(entry.getKey()); - buf.append(":"); - buf.append(entry.getValue()); - buf.append("
    "); - } - htmlString = buf.toString(); + if (htmlString != null) { + return htmlString; } + + Set> entries = entrySet(); + if (entries.isEmpty()) { + return ""; // empty so tooltip clients can handle empty data + } + + StringBuilder buf = new StringBuilder(""); + for (Map.Entry entry : entries) { + buf.append(entry.getKey()); + buf.append(":"); + buf.append(StringEscapeUtils.escapeHtml4(entry.getValue())); + buf.append("
    "); + } + htmlString = buf.toString(); return htmlString; } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java index 0ca6a83508..5a44c0c765 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/service/graph/AttributedVertex.java @@ -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(""); - for (Map.Entry entry : entrySet()) { - buf.append(entry.getKey()); - buf.append(":"); - buf.append(StringEscapeUtils.escapeHtml4(entry.getValue())); - buf.append("
    "); - } - htmlString = buf.toString(); + + if (htmlString != null) { + return htmlString; } + + Set> entries = entrySet(); + if (entries.isEmpty()) { + return ""; // empty so tooltip clients can handle empty data + } + + StringBuilder buf = new StringBuilder(""); + for (Map.Entry entry : entries) { + buf.append(entry.getKey()); + buf.append(":"); + buf.append(entry.getValue()); + buf.append("
    "); + } + htmlString = buf.toString(); return htmlString; }