diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java new file mode 100644 index 0000000000..a247e899b2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java @@ -0,0 +1,92 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser.hover; + +import javax.swing.JComponent; +import javax.swing.JToolTip; + +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.GhidraOptions; +import ghidra.app.plugin.core.hover.AbstractConfigurableHover; +import ghidra.app.util.ToolTipUtils; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.*; + +/** + * A Listing hover to show tool tips for function signatures + */ +public class FunctionSignatureListingHover extends AbstractConfigurableHover + implements ListingHoverService { + + private static final String NAME = "Function Signature Display"; + private static final String DESCRIPTION = + "Toggle whether function signature is displayed in a tooltip " + + "when the mouse hovers over a function signature."; + + // note: guilty knowledge that the Truncated Text service has a priority of 10 + private static final int POPUP_PRIORITY = 20; + + public FunctionSignatureListingHover(PluginTool tool) { + super(tool, POPUP_PRIORITY); + } + + @Override + protected String getName() { + return NAME; + } + + @Override + protected String getDescription() { + return DESCRIPTION; + } + + @Override + protected String getOptionsCategory() { + return GhidraOptions.CATEGORY_BROWSER_POPUPS; + } + + @Override + public JComponent getHoverComponent(Program program, ProgramLocation programLocation, + FieldLocation fieldLocation, Field field) { + + if (!enabled || programLocation == null) { + return null; + } + + Class clazz = programLocation.getClass(); + if (clazz != FunctionSignatureFieldLocation.class && + clazz != FunctionNameFieldLocation.class) { + return null; + } + + // is the label local to the function + FunctionSignatureFieldLocation functionLocation = + (FunctionSignatureFieldLocation) programLocation; + + Address entry = functionLocation.getFunctionAddress(); + FunctionManager functionManager = program.getFunctionManager(); + Function function = functionManager.getFunctionAt(entry); + + String toolTipText = ToolTipUtils.getToolTipText(function, true); + JToolTip toolTip = new JToolTip(); + toolTip.setTipText(toolTipText); + return toolTip; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java new file mode 100644 index 0000000000..301c30bf5d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java @@ -0,0 +1,51 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser.hover; + +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * A plugin to show tool tip text for a function signature + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.CODE_VIEWER, + shortDescription = "Shows formatted tool tip text over function signatures", + description = "This plugin extends the functionality of the code browser by adding a " + + "\tooltip\" over function signaturefields in Listing.", + servicesProvided = { ListingHoverService.class } +) +//@formatter:on +public class FunctionSignatureListingHoverPlugin extends Plugin { + + private FunctionSignatureListingHover functionSignatureHover; + + public FunctionSignatureListingHoverPlugin(PluginTool tool) { + super(tool); + functionSignatureHover = new FunctionSignatureListingHover(tool); + registerServiceProvided(ListingHoverService.class, functionSignatureHover); + } + + @Override + protected void dispose() { + functionSignatureHover.dispose(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java index 2a4a22ac0d..78139cbc5d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java @@ -123,9 +123,9 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable return; } - String dataDescr = "Data Offset"; + String description = "Data Offset"; if (data.getDataType() instanceof Structure) { - dataDescr = "Structure Offset"; + description = "Structure Offset"; } String name = data.getLabel(); // prefer the label @@ -133,12 +133,14 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable name = data.getDataType().getName(); } + name = StringUtilities.trimMiddle(name, 60); + if (name == null) { // don't think we can get here name = italic("Unnamed"); } - appendTableRow(sb, dataDescr, name, dataOffset); + appendTableRow(sb, description, name, dataOffset); } private void addByteSourceInfo(Program program, Address loc, StringBuilder sb) { @@ -150,7 +152,8 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable if (addressSourceInfo.getFileName() == null) { return; } - String filename = StringUtilities.trim(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE); + String filename = + StringUtilities.trimMiddle(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE); long fileOffset = addressSourceInfo.getFileOffset(); String dataDescr = "Byte Source Offset"; appendTableRow(sb, dataDescr, "File: " + filename, fileOffset); @@ -160,7 +163,10 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable Function function = program.getFunctionManager().getFunctionContaining(loc); if (function != null) { long functionOffset = loc.subtract(function.getEntryPoint()); - appendTableRow(sb, "Function Offset", HTMLUtilities.escapeHTML(function.getName()), + + String functionName = function.getName(); + functionName = StringUtilities.trimMiddle(functionName, 60); + appendTableRow(sb, "Function Offset", HTMLUtilities.escapeHTML(functionName), functionOffset); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java index 54f9c71394..0da7e2667a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java @@ -91,14 +91,15 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover { String hoverName = getName(); options.getOptions(hoverName).setOptionsHelpLocation(help); + options.registerOption(hoverName, true, null, getDescription()); + enabled = options.getBoolean(hoverName, true); options.registerOption(hoverName + Options.DELIMITER + "Dialog Height", 400, help, "Height of the popup window"); options.registerOption(hoverName + Options.DELIMITER + "Dialog Width", 600, help, "Width of the popup window"); - setOptions(options, hoverName); options.addOptionsChangeListener(this); } @@ -152,10 +153,18 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover { return; } + toolTip = new JToolTip(); + panel = new ListingPanel(codeFormatService.getFormatManager());// share the manager from the code viewer panel.setTextBackgroundColor(BACKGROUND_COLOR); - toolTip = new JToolTip(); + String name = getName(); + String widthOptionName = name + Options.DELIMITER + "Dialog Width"; + String heightOptionName = name + Options.DELIMITER + "Dialog Height"; + int dialogWidth = options.getInt(widthOptionName, 600); + int dialogHeight = options.getInt(heightOptionName, 400); + Dimension d = new Dimension(dialogWidth, dialogHeight); + panel.setPreferredSize(d); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java index 434a2cbb1c..c303b0b6b6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java @@ -15,13 +15,12 @@ */ package ghidra.app.services; -import ghidra.program.model.listing.Program; -import ghidra.program.util.ProgramLocation; - import javax.swing.JComponent; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; /** * HoverService provides the ability to popup data Windows over a Field viewer @@ -30,7 +29,8 @@ import docking.widgets.fieldpanel.support.FieldLocation; public interface HoverService { /** - * Returns the priority of this hover service. + * Returns the priority of this hover service. A lower priority is more important. + * @return the priority */ public int getPriority(); @@ -41,7 +41,8 @@ public interface HoverService { public void scroll(int amount); /** - * Return whether hover mode is "on." + * Return whether hover mode is "on" + * @return the priority */ public boolean hoverModeSelected(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java index 7af4d68ce8..e29c102bc2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java @@ -17,6 +17,7 @@ package docking.widgets; import java.awt.*; import java.awt.event.*; +import java.awt.geom.Rectangle2D; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; @@ -25,7 +26,9 @@ import java.util.List; import javax.swing.*; import javax.swing.Timer; +import generic.json.Json; import generic.util.WindowUtilities; +import ghidra.util.Msg; import ghidra.util.bean.GGlassPane; import ghidra.util.bean.GGlassPanePainter; @@ -112,8 +115,8 @@ public class PopupWindow { sourceMouseMotionListener = new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { - Point localPoint = e.getPoint(); + Point localPoint = e.getPoint(); SwingUtilities.convertPointToScreen(localPoint, e.getComponent()); if (!neutralMotionZone.contains(localPoint)) { hide(); @@ -121,7 +124,7 @@ public class PopupWindow { else { // If the user mouses around the neutral zone, then start the close timer. The // timer will be reset if the user enters the popup. - closeTimer.start(); + closeTimer.restart(); } e.consume(); // consume the event so that the source component doesn't processes it } @@ -237,16 +240,15 @@ public class PopupWindow { int y = point.y + keepVisibleArea.height + Y_PADDING; popupBounds.setLocation(x, y); - WindowUtilities.ensureOnScreen(sourceComponent, popupBounds); + ensureSize(popupBounds); Rectangle hoverArea = new Rectangle(point, keepVisibleArea); - adjustBoundsForCursorLocation(popupBounds, hoverArea); - - neutralMotionZone = createNeutralMotionZone(popupBounds, hoverArea); + Rectangle adjustedBounds = adjustBoundsForCursorLocation(popupBounds, hoverArea); + neutralMotionZone = createNeutralMotionZone(adjustedBounds, hoverArea); installDebugPainter(e); - popup.setBounds(popupBounds); + popup.setBounds(adjustedBounds); popup.setVisible(true); removeOldPopupReferences(); @@ -254,19 +256,38 @@ public class PopupWindow { VISIBLE_POPUPS.add(new WeakReference<>(this)); } + private void ensureSize(Rectangle popupBounds) { + Shape screenShape = WindowUtilities.getVisibleScreenBounds(); + Rectangle screenBounds = screenShape.getBounds(); + if (screenBounds.width < popupBounds.width) { + popupBounds.width = screenBounds.width / 2; + } + + if (screenBounds.height < popupBounds.height) { + popupBounds.height = screenBounds.height / 2; + } + } + + private void installDebugPainter(List grid) { +// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent); +// ShapeDebugPainter painter = new ShapeDebugPainter(null, grid, neutralMotionZone); +// glassPane.addPainter(painter); +// painters.add(painter); + } + private void installDebugPainter(MouseEvent e) { - // GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent); - // ShapeDebugPainter painter = new ShapeDebugPainter(e, neutralMotionZone); - // glassPane.addPainter(painter); +// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent); +// ShapeDebugPainter painter = new ShapeDebugPainter(e, null, neutralMotionZone); +// glassPane.addPainter(painter); +// painters.add(painter); } /** * Adjusts the given bounds to make sure that they do not cover the given location. *

- * When the hoverArea is obscured, this method will first attempt to move the - * bounds up if possible. If moving up is not possible due to space constraints, then this - * method will try to shift the bounds to the right of the hover area. If this is not - * possible, then the bounds will not be changed. + * When the hoverArea is obscured, this method will create a grid of possible locations + * in which to place the given bounds. The grid will be searched for the location that is + * closest to the hover area without touching it. * * @param bounds The bounds to move as necessary. * @param hoverArea The area that should not be covered by the given bounds @@ -274,28 +295,113 @@ public class PopupWindow { * if possible. */ private Rectangle adjustBoundsForCursorLocation(Rectangle bounds, Rectangle hoverArea) { - if (!bounds.intersects(hoverArea)) { + Shape screenShape = WindowUtilities.getVisibleScreenBounds(); + Rectangle screenBounds = screenShape.getBounds(); + if (!bounds.intersects(hoverArea) && screenBounds.contains(bounds)) { return bounds; } - // first attempt to move the window--try to go up - int movedY = hoverArea.y - bounds.height; - boolean canMoveUp = movedY >= 0; - if (canMoveUp) { - // move the given bounds above the current point - bounds.y = movedY; + // center bounds over hover area; we intend not to block the hover area + int dx = (hoverArea.width / 2) - (bounds.width / 2); + int dy = (hoverArea.height / 2) - (bounds.height / 2); + Point hoverCenter = bounds.getLocation(); + hoverCenter.x += dx; + hoverCenter.y += dy; + + List grid = createSortedGrid(screenBounds, hoverCenter, bounds.getSize()); + + installDebugPainter(grid); + + // try placing the bounds in each grid cell in order until no clipping + Rectangle match = null; + for (GridCell cell : grid) { + Rectangle r = cell.getRectangle(); + if (!hoverArea.intersects(r) && + screenBounds.contains(r)) { + match = r; + break; + } + } + + if (match == null) { + Msg.debug(null, "Could not find a place to put the rectangle" + bounds); return bounds; } - // We couldn't move up, so we try to go left, since by default the popup is placed - // to the right of the hover area. - int movedX = hoverArea.x - bounds.width; - boolean canMoveLeft = movedX >= 0; - if (canMoveLeft) { - bounds.x = movedX; + return match; + } + + private List createSortedGrid(Rectangle screen, Point targetCenter, Dimension size) { + + List grid = new ArrayList<>(); + + // + // Rather than just a simple grid of rows and columns of the given size, this loop will + // create twice as many rows and columns, each half the size, resulting in 4 time the + // number of potential locations. This allows for potential closer placement to the + // target center. + // + int row = 0; + Rectangle r = new Rectangle(new Point(0, 0), size); + while (screen.contains(r)) { + + int x = r.x; + int y = r.y; + + int col = 0; + while (screen.contains(r)) { + grid.add(new GridCell(r, targetCenter, row, col)); + + // add another cell halfway over + col++; + int half = size.width / 2; + x += half; + r = new Rectangle(x, r.y, r.width, r.height); + + if (screen.contains(r)) { + grid.add(new GridCell(r, targetCenter, row, col)); + } + + col++; + x += half; + r = new Rectangle(x, r.y, r.width, r.height); + } + + row++; + x = 0; + int halfHeight = r.height / 2; + y += halfHeight; + r = new Rectangle(x, y, r.width, r.height); + + // loop again for another row halfway down + col = 0; + while (screen.contains(r)) { + grid.add(new GridCell(r, targetCenter, row, col)); + + // add another cell halfway over + col++; + int half = size.width / 2; + x += half; + r = new Rectangle(x, r.y, r.width, r.height); + + if (screen.contains(r)) { + grid.add(new GridCell(r, targetCenter, row, col)); + } + + col++; + x += half; + r = new Rectangle(x, r.y, r.width, r.height); + } + + row++; + x = 0; + y += halfHeight; + r = new Rectangle(x, y, r.width, r.height); } - return bounds; + grid.sort(null); + + return grid; } /** @@ -336,6 +442,70 @@ public class PopupWindow { return abs2 - abs1; } +//================================================================================================== +// Inner Classes +//================================================================================================== + + /** + * A cell of a grid. This cell is given the target center point of the screen space, which + * is used to determine how close this cell is to the target. + */ + private class GridCell implements Comparable { + private Rectangle rectangle; + private int distanceFromCenter; + private boolean isRight; + private boolean isBelow; + private int row; + private int col; + + GridCell(Rectangle rectangle, Point targetCenter, int row, int col) { + this.rectangle = rectangle; + this.row = row; + this.col = col; + + double cx = rectangle.getCenterX(); + double cy = rectangle.getCenterY(); + double scx = targetCenter.getX(); + double scy = targetCenter.getY(); + double dx = cx - scx; + double dy = cy - scy; + distanceFromCenter = (int) Math.sqrt(dx * dx + dy * dy); + isRight = cx > scx; + isBelow = cy > scy; + } + + public Rectangle getRectangle() { + return rectangle; + } + + @Override + public int compareTo(GridCell other) { + // smaller distances come first + int delta = distanceFromCenter - other.distanceFromCenter; + if (delta != 0) { + return delta; + } + + if (isRight != other.isRight) { + return isRight ? -1 : 1; // prefer right side + } + + if (isBelow != other.isBelow) { + return isBelow ? -1 : 1; // prefer below + } + + return 0; + } + + @Override + public String toString() { + return Json.toString(this); + } + } + + // for debug + //private static List painters = new ArrayList<>(); + /** Paints shapes used by this class (useful for debugging) */ @SuppressWarnings("unused") // enabled as needed @@ -343,30 +513,77 @@ public class PopupWindow { private MouseEvent sourceEvent; private Rectangle bounds; + private List grid; - ShapeDebugPainter(MouseEvent sourceEvent, Rectangle bounds) { + ShapeDebugPainter(MouseEvent sourceEvent, List grid, Rectangle bounds) { this.sourceEvent = sourceEvent; + this.grid = grid; this.bounds = bounds; } @Override - public void paint(GGlassPane glassPane, Graphics graphics) { + public void paint(GGlassPane glassPane, Graphics g) { // bounds of the popup and the mouse neutral zone - Rectangle r = bounds; - Point p = new Point(r.getLocation()); - SwingUtilities.convertPointFromScreen(p, glassPane); + if (bounds != null) { + Rectangle r = bounds; + Point p = new Point(r.getLocation()); + SwingUtilities.convertPointFromScreen(p, glassPane); - Color c = new Color(50, 50, 200, 125); - graphics.setColor(c); - graphics.fillRect(p.x, p.y, r.width, r.height); + Color c = new Color(50, 50, 200, 125); + g.setColor(c); + g.fillRect(p.x, p.y, r.width, r.height); + } // show where the user hovered - p = sourceEvent.getPoint(); - p = SwingUtilities.convertPoint(sourceEvent.getComponent(), p.x, p.y, glassPane); - graphics.setColor(Color.RED); - int offset = 10; - graphics.fillRect(p.x - offset, p.y - offset, (offset * 2), (offset * 2)); + if (sourceEvent != null) { + Point p = sourceEvent.getPoint(); + p = SwingUtilities.convertPoint(sourceEvent.getComponent(), p.x, p.y, glassPane); + g.setColor(Color.RED); + int offset = 10; + g.fillRect(p.x - offset, p.y - offset, (offset * 2), (offset * 2)); + } + + if (grid != null) { + + Graphics2D g2d = (Graphics2D) g; + Font font = g2d.getFont().deriveFont(12).deriveFont(Font.BOLD); + g2d.setFont(font); + + g.setColor(new Color(55, 0, 0, 100)); + for (GridCell cell : grid) { + + Rectangle r = cell.getRectangle(); + Point p = r.getLocation(); + int oldY = p.y; + SwingUtilities.convertPointFromScreen(p, glassPane); + int x = p.x; + int y = p.y; + int w = r.width; + int h = r.height; + g2d.fillRect(x, y, w, h); + } + + g2d.setColor(Color.PINK); + for (GridCell cell : grid) { + String coord = "(" + cell.row + "," + cell.col + ")"; + Rectangle r = cell.getRectangle(); + + int cx = r.x + r.width; + int cy = r.y + r.height; + Point p = new Point(cx, cy); + SwingUtilities.convertPointFromScreen(p, glassPane); + FontMetrics fm = g2d.getFontMetrics(); + Rectangle2D sbounds = fm.getStringBounds(coord, g2d); + int textWidth = (int) sbounds.getWidth(); + int textHeight = (int) sbounds.getHeight(); + int scx = (int) sbounds.getCenterX(); + int scy = (int) sbounds.getCenterY(); + int textx = p.x - textWidth; + int texty = p.y - textHeight; + g2d.drawString(coord, p.x, p.y); + } + } } } }