mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GP-875 - Hovers - Fixed how we choose locations to display the reference
hover window; added a formatted tooltip provider for function signatures; fixed a bug that prevented the reference hover from being correctly sized
This commit is contained in:
parent
1b0bd54560
commit
a9acf8bdf8
6 changed files with 429 additions and 53 deletions
|
@ -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<? extends ProgramLocation> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,9 +123,9 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String dataDescr = "Data Offset";
|
String description = "Data Offset";
|
||||||
if (data.getDataType() instanceof Structure) {
|
if (data.getDataType() instanceof Structure) {
|
||||||
dataDescr = "Structure Offset";
|
description = "Structure Offset";
|
||||||
}
|
}
|
||||||
|
|
||||||
String name = data.getLabel(); // prefer the label
|
String name = data.getLabel(); // prefer the label
|
||||||
|
@ -133,12 +133,14 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable
|
||||||
name = data.getDataType().getName();
|
name = data.getDataType().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name = StringUtilities.trimMiddle(name, 60);
|
||||||
|
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
// don't think we can get here
|
// don't think we can get here
|
||||||
name = italic("Unnamed");
|
name = italic("Unnamed");
|
||||||
}
|
}
|
||||||
|
|
||||||
appendTableRow(sb, dataDescr, name, dataOffset);
|
appendTableRow(sb, description, name, dataOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addByteSourceInfo(Program program, Address loc, StringBuilder sb) {
|
private void addByteSourceInfo(Program program, Address loc, StringBuilder sb) {
|
||||||
|
@ -150,7 +152,8 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable
|
||||||
if (addressSourceInfo.getFileName() == null) {
|
if (addressSourceInfo.getFileName() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String filename = StringUtilities.trim(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE);
|
String filename =
|
||||||
|
StringUtilities.trimMiddle(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE);
|
||||||
long fileOffset = addressSourceInfo.getFileOffset();
|
long fileOffset = addressSourceInfo.getFileOffset();
|
||||||
String dataDescr = "Byte Source Offset";
|
String dataDescr = "Byte Source Offset";
|
||||||
appendTableRow(sb, dataDescr, "File: " + filename, fileOffset);
|
appendTableRow(sb, dataDescr, "File: " + filename, fileOffset);
|
||||||
|
@ -160,7 +163,10 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable
|
||||||
Function function = program.getFunctionManager().getFunctionContaining(loc);
|
Function function = program.getFunctionManager().getFunctionContaining(loc);
|
||||||
if (function != null) {
|
if (function != null) {
|
||||||
long functionOffset = loc.subtract(function.getEntryPoint());
|
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);
|
functionOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,14 +91,15 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover {
|
||||||
|
|
||||||
String hoverName = getName();
|
String hoverName = getName();
|
||||||
options.getOptions(hoverName).setOptionsHelpLocation(help);
|
options.getOptions(hoverName).setOptionsHelpLocation(help);
|
||||||
|
|
||||||
options.registerOption(hoverName, true, null, getDescription());
|
options.registerOption(hoverName, true, null, getDescription());
|
||||||
|
enabled = options.getBoolean(hoverName, true);
|
||||||
|
|
||||||
options.registerOption(hoverName + Options.DELIMITER + "Dialog Height", 400, help,
|
options.registerOption(hoverName + Options.DELIMITER + "Dialog Height", 400, help,
|
||||||
"Height of the popup window");
|
"Height of the popup window");
|
||||||
options.registerOption(hoverName + Options.DELIMITER + "Dialog Width", 600, help,
|
options.registerOption(hoverName + Options.DELIMITER + "Dialog Width", 600, help,
|
||||||
"Width of the popup window");
|
"Width of the popup window");
|
||||||
|
|
||||||
setOptions(options, hoverName);
|
|
||||||
options.addOptionsChangeListener(this);
|
options.addOptionsChangeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,10 +153,18 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolTip = new JToolTip();
|
||||||
|
|
||||||
panel = new ListingPanel(codeFormatService.getFormatManager());// share the manager from the code viewer
|
panel = new ListingPanel(codeFormatService.getFormatManager());// share the manager from the code viewer
|
||||||
panel.setTextBackgroundColor(BACKGROUND_COLOR);
|
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
|
@Override
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.app.services;
|
package ghidra.app.services;
|
||||||
|
|
||||||
import ghidra.program.model.listing.Program;
|
|
||||||
import ghidra.program.util.ProgramLocation;
|
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
import docking.widgets.fieldpanel.field.Field;
|
import docking.widgets.fieldpanel.field.Field;
|
||||||
import docking.widgets.fieldpanel.support.FieldLocation;
|
import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.util.ProgramLocation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <code>HoverService</code> provides the ability to popup data Windows over a Field viewer
|
* <code>HoverService</code> provides the ability to popup data Windows over a Field viewer
|
||||||
|
@ -30,7 +29,8 @@ import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
public interface HoverService {
|
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();
|
public int getPriority();
|
||||||
|
|
||||||
|
@ -41,7 +41,8 @@ public interface HoverService {
|
||||||
public void scroll(int amount);
|
public void scroll(int amount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether hover mode is "on."
|
* Return whether hover mode is "on"
|
||||||
|
* @return the priority
|
||||||
*/
|
*/
|
||||||
public boolean hoverModeSelected();
|
public boolean hoverModeSelected();
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ package docking.widgets;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -25,7 +26,9 @@ import java.util.List;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
|
|
||||||
|
import generic.json.Json;
|
||||||
import generic.util.WindowUtilities;
|
import generic.util.WindowUtilities;
|
||||||
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.bean.GGlassPane;
|
import ghidra.util.bean.GGlassPane;
|
||||||
import ghidra.util.bean.GGlassPanePainter;
|
import ghidra.util.bean.GGlassPanePainter;
|
||||||
|
|
||||||
|
@ -112,8 +115,8 @@ public class PopupWindow {
|
||||||
sourceMouseMotionListener = new MouseMotionAdapter() {
|
sourceMouseMotionListener = new MouseMotionAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mouseMoved(MouseEvent e) {
|
public void mouseMoved(MouseEvent e) {
|
||||||
Point localPoint = e.getPoint();
|
|
||||||
|
|
||||||
|
Point localPoint = e.getPoint();
|
||||||
SwingUtilities.convertPointToScreen(localPoint, e.getComponent());
|
SwingUtilities.convertPointToScreen(localPoint, e.getComponent());
|
||||||
if (!neutralMotionZone.contains(localPoint)) {
|
if (!neutralMotionZone.contains(localPoint)) {
|
||||||
hide();
|
hide();
|
||||||
|
@ -121,7 +124,7 @@ public class PopupWindow {
|
||||||
else {
|
else {
|
||||||
// If the user mouses around the neutral zone, then start the close timer. The
|
// If the user mouses around the neutral zone, then start the close timer. The
|
||||||
// timer will be reset if the user enters the popup.
|
// 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
|
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;
|
int y = point.y + keepVisibleArea.height + Y_PADDING;
|
||||||
popupBounds.setLocation(x, y);
|
popupBounds.setLocation(x, y);
|
||||||
|
|
||||||
WindowUtilities.ensureOnScreen(sourceComponent, popupBounds);
|
ensureSize(popupBounds);
|
||||||
|
|
||||||
Rectangle hoverArea = new Rectangle(point, keepVisibleArea);
|
Rectangle hoverArea = new Rectangle(point, keepVisibleArea);
|
||||||
adjustBoundsForCursorLocation(popupBounds, hoverArea);
|
Rectangle adjustedBounds = adjustBoundsForCursorLocation(popupBounds, hoverArea);
|
||||||
|
neutralMotionZone = createNeutralMotionZone(adjustedBounds, hoverArea);
|
||||||
neutralMotionZone = createNeutralMotionZone(popupBounds, hoverArea);
|
|
||||||
|
|
||||||
installDebugPainter(e);
|
installDebugPainter(e);
|
||||||
|
|
||||||
popup.setBounds(popupBounds);
|
popup.setBounds(adjustedBounds);
|
||||||
popup.setVisible(true);
|
popup.setVisible(true);
|
||||||
|
|
||||||
removeOldPopupReferences();
|
removeOldPopupReferences();
|
||||||
|
@ -254,19 +256,38 @@ public class PopupWindow {
|
||||||
VISIBLE_POPUPS.add(new WeakReference<>(this));
|
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<GridCell> grid) {
|
||||||
|
// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent);
|
||||||
|
// ShapeDebugPainter painter = new ShapeDebugPainter(null, grid, neutralMotionZone);
|
||||||
|
// glassPane.addPainter(painter);
|
||||||
|
// painters.add(painter);
|
||||||
|
}
|
||||||
|
|
||||||
private void installDebugPainter(MouseEvent e) {
|
private void installDebugPainter(MouseEvent e) {
|
||||||
// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent);
|
// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent);
|
||||||
// ShapeDebugPainter painter = new ShapeDebugPainter(e, neutralMotionZone);
|
// ShapeDebugPainter painter = new ShapeDebugPainter(e, null, neutralMotionZone);
|
||||||
// glassPane.addPainter(painter);
|
// glassPane.addPainter(painter);
|
||||||
|
// painters.add(painter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjusts the given bounds to make sure that they do not cover the given location.
|
* Adjusts the given bounds to make sure that they do not cover the given location.
|
||||||
* <p>
|
* <p>
|
||||||
* When the <tt>hoverArea</tt> is obscured, this method will first attempt to move the
|
* When the <tt>hoverArea</tt> is obscured, this method will create a grid of possible locations
|
||||||
* bounds up if possible. If moving up is not possible due to space constraints, then this
|
* in which to place the given bounds. The grid will be searched for the location that is
|
||||||
* method will try to shift the bounds to the right of the hover area. If this is not
|
* closest to the hover area without touching it.
|
||||||
* possible, then the bounds will not be changed.
|
|
||||||
*
|
*
|
||||||
* @param bounds The bounds to move as necessary.
|
* @param bounds The bounds to move as necessary.
|
||||||
* @param hoverArea The area that should not be covered by the given bounds
|
* @param hoverArea The area that should not be covered by the given bounds
|
||||||
|
@ -274,28 +295,113 @@ public class PopupWindow {
|
||||||
* if possible.
|
* if possible.
|
||||||
*/
|
*/
|
||||||
private Rectangle adjustBoundsForCursorLocation(Rectangle bounds, Rectangle hoverArea) {
|
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;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// first attempt to move the window--try to go up
|
// center bounds over hover area; we intend not to block the hover area
|
||||||
int movedY = hoverArea.y - bounds.height;
|
int dx = (hoverArea.width / 2) - (bounds.width / 2);
|
||||||
boolean canMoveUp = movedY >= 0;
|
int dy = (hoverArea.height / 2) - (bounds.height / 2);
|
||||||
if (canMoveUp) {
|
Point hoverCenter = bounds.getLocation();
|
||||||
// move the given bounds above the current point
|
hoverCenter.x += dx;
|
||||||
bounds.y = movedY;
|
hoverCenter.y += dy;
|
||||||
|
|
||||||
|
List<GridCell> 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;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We couldn't move up, so we try to go left, since by default the popup is placed
|
return match;
|
||||||
// to the right of the hover area.
|
}
|
||||||
int movedX = hoverArea.x - bounds.width;
|
|
||||||
boolean canMoveLeft = movedX >= 0;
|
private List<GridCell> createSortedGrid(Rectangle screen, Point targetCenter, Dimension size) {
|
||||||
if (canMoveLeft) {
|
|
||||||
bounds.x = movedX;
|
List<GridCell> 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;
|
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<GridCell> {
|
||||||
|
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<GGlassPanePainter> painters = new ArrayList<>();
|
||||||
|
|
||||||
/** Paints shapes used by this class (useful for debugging) */
|
/** Paints shapes used by this class (useful for debugging) */
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
// enabled as needed
|
// enabled as needed
|
||||||
|
@ -343,30 +513,77 @@ public class PopupWindow {
|
||||||
|
|
||||||
private MouseEvent sourceEvent;
|
private MouseEvent sourceEvent;
|
||||||
private Rectangle bounds;
|
private Rectangle bounds;
|
||||||
|
private List<GridCell> grid;
|
||||||
|
|
||||||
ShapeDebugPainter(MouseEvent sourceEvent, Rectangle bounds) {
|
ShapeDebugPainter(MouseEvent sourceEvent, List<GridCell> grid, Rectangle bounds) {
|
||||||
this.sourceEvent = sourceEvent;
|
this.sourceEvent = sourceEvent;
|
||||||
|
this.grid = grid;
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void paint(GGlassPane glassPane, Graphics graphics) {
|
public void paint(GGlassPane glassPane, Graphics g) {
|
||||||
|
|
||||||
// bounds of the popup and the mouse neutral zone
|
// bounds of the popup and the mouse neutral zone
|
||||||
Rectangle r = bounds;
|
if (bounds != null) {
|
||||||
Point p = new Point(r.getLocation());
|
Rectangle r = bounds;
|
||||||
SwingUtilities.convertPointFromScreen(p, glassPane);
|
Point p = new Point(r.getLocation());
|
||||||
|
SwingUtilities.convertPointFromScreen(p, glassPane);
|
||||||
|
|
||||||
Color c = new Color(50, 50, 200, 125);
|
Color c = new Color(50, 50, 200, 125);
|
||||||
graphics.setColor(c);
|
g.setColor(c);
|
||||||
graphics.fillRect(p.x, p.y, r.width, r.height);
|
g.fillRect(p.x, p.y, r.width, r.height);
|
||||||
|
}
|
||||||
|
|
||||||
// show where the user hovered
|
// show where the user hovered
|
||||||
p = sourceEvent.getPoint();
|
if (sourceEvent != null) {
|
||||||
p = SwingUtilities.convertPoint(sourceEvent.getComponent(), p.x, p.y, glassPane);
|
Point p = sourceEvent.getPoint();
|
||||||
graphics.setColor(Color.RED);
|
p = SwingUtilities.convertPoint(sourceEvent.getComponent(), p.x, p.y, glassPane);
|
||||||
int offset = 10;
|
g.setColor(Color.RED);
|
||||||
graphics.fillRect(p.x - offset, p.y - offset, (offset * 2), (offset * 2));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue