Merge remote-tracking branch 'origin/GP-875-dragonmacher-hover-popup-location' into Ghidra_10.0

This commit is contained in:
ghidra1
2021-06-09 09:25:16 -04:00
11 changed files with 2317 additions and 138 deletions

View File

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

View File

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

View File

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

View File

@@ -192,9 +192,20 @@ public abstract class AbstractHoverProvider implements HoverProvider {
popupWindow.showPopup(event);
}
else {
int xOffset = 50;// magic: trial and error
Dimension size = fieldBounds.getSize();
Dimension keepVisibleArea = new Dimension(xOffset, size.height);
//
// Make an area over which to show the popup. The popup should not cover this area.
// The field that is hovered may be too big to be this area, as a big field may cause
// the popup to be too far away from the cursor.
//
// Use the mouse point and then create an area (based on trial-and-error) that should
// not be occluded.
//
int horizontalPad = 100;
int verticalPad = 50;
Rectangle keepVisibleArea = new Rectangle(event.getPoint());
keepVisibleArea.grow(horizontalPad, verticalPad);
popupWindow.showOffsetPopup(event, keepVisibleArea);
}
}

View File

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

View File

@@ -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;
/**
* <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 {
/**
* 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();

View File

@@ -25,13 +25,19 @@ import java.util.List;
import javax.swing.*;
import javax.swing.Timer;
import docking.widgets.shapes.*;
import generic.util.WindowUtilities;
import ghidra.util.bean.GGlassPane;
import ghidra.util.bean.GGlassPanePainter;
/**
* A generic window intended to be used as a temporary window to show information. This window is
* designed to stay open as long as the user mouses over the window. Once the user mouses away,
* the window will be closed.
*/
public class PopupWindow {
private static final int X_PADDING = 20;
private static final int Y_PADDING = 20;
private static final int X_PADDING = 25;
private static final int Y_PADDING = 25;
private static final List<WeakReference<PopupWindow>> VISIBLE_POPUPS = new ArrayList<>();
public static void hideAllWindows() {
@@ -43,11 +49,20 @@ public class PopupWindow {
}
}
private JWindow popup;
private static final PopupWindowPlacer DEFAULT_WINDOW_PLACER =
new PopupWindowPlacerBuilder()
.rightEdge(Location.BOTTOM)
.leftEdge(Location.BOTTOM)
.bottomEdge(Location.RIGHT)
.topEdge(Location.CENTER)
.leastOverlapCorner()
.throwsAssertException()
.build();
private Component sourceComponent;
/** Area where user can mouse without hiding the window (in screen coordinates) */
private Rectangle neutralMotionZone;
private Rectangle mouseMovementArea;
private JWindow popup;
private Component sourceComponent;
private MouseMotionListener sourceMouseMotionListener;
private MouseListener sourceMouseListener;
@@ -87,7 +102,7 @@ public class PopupWindow {
popup = new JWindow(parentWindow);
popup.setFocusableWindowState(false);
// this is bad, as it keeps tooltips above all apps and they don't go away, as normal tooltips do
// this is bad, as it keeps tooltips above all apps and they don't go away, as normal tooltips do
// popup.setAlwaysOnTop( true );
popup.getContentPane().add(displayComponent);
@@ -112,16 +127,16 @@ 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)) {
if (!mouseMovementArea.contains(localPoint)) {
hide();
}
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
}
@@ -183,8 +198,8 @@ public class PopupWindow {
}
private void removeOldPopupReferences() {
for (Iterator<WeakReference<PopupWindow>> iterator =
VISIBLE_POPUPS.iterator(); iterator.hasNext();) {
for (Iterator<WeakReference<PopupWindow>> iterator = VISIBLE_POPUPS.iterator(); iterator
.hasNext();) {
WeakReference<PopupWindow> reference = iterator.next();
PopupWindow window = reference.get();
if (window == this) {
@@ -202,7 +217,7 @@ public class PopupWindow {
/**
* Sets the amount of time that will pass before the popup window is closed <b>after</b> the
* user moves away from the popup window and out of the neutral zone
*
*
* @param delayInMillis the timer delay
*/
public void setCloseWindowDelay(int delayInMillis) {
@@ -210,43 +225,32 @@ public class PopupWindow {
closeTimer.setRepeats(false);
}
public void showOffsetPopup(MouseEvent e, Dimension keepVisibleArea) {
doShowPopup(e, keepVisibleArea);
public void showOffsetPopup(MouseEvent e, Rectangle keepVisibleSize) {
doShowPopup(e, keepVisibleSize, DEFAULT_WINDOW_PLACER);
}
public void showPopup(MouseEvent e) {
doShowPopup(e, null);
doShowPopup(e, null, DEFAULT_WINDOW_PLACER);
}
private void doShowPopup(MouseEvent e, Dimension keepVisibleArea) {
private void doShowPopup(MouseEvent e, Rectangle keepVisibleSize, PopupWindowPlacer placer) {
hideAllWindows();
sourceComponent = e.getComponent();
sourceComponent.addMouseListener(sourceMouseListener);
sourceComponent.addMouseMotionListener(sourceMouseMotionListener);
Point point = e.getPoint();
SwingUtilities.convertPointToScreen(point, sourceComponent);
if (keepVisibleArea == null) {
keepVisibleArea = new Dimension(0, 0);
}
Dimension popupDimension = popup.getSize();
ensureSize(popupDimension);
Rectangle popupBounds = popup.getBounds();
int x = point.x + keepVisibleArea.width + X_PADDING;
int y = point.y + keepVisibleArea.height + Y_PADDING;
popupBounds.setLocation(x, y);
WindowUtilities.ensureOnScreen(sourceComponent, popupBounds);
Rectangle hoverArea = new Rectangle(point, keepVisibleArea);
adjustBoundsForCursorLocation(popupBounds, hoverArea);
neutralMotionZone = createNeutralMotionZone(popupBounds, hoverArea);
Rectangle keepVisibleArea = createKeepVisibleArea(e, keepVisibleSize);
Rectangle screenBounds = WindowUtilities.getVisibleScreenBounds().getBounds();
Rectangle placement = placer.getPlacement(popupDimension, keepVisibleArea, screenBounds);
mouseMovementArea = createMovementArea(placement, keepVisibleArea);
installDebugPainter(e);
popup.setBounds(popupBounds);
popup.setBounds(placement);
popup.setVisible(true);
removeOldPopupReferences();
@@ -254,87 +258,62 @@ public class PopupWindow {
VISIBLE_POPUPS.add(new WeakReference<>(this));
}
private Rectangle createKeepVisibleArea(MouseEvent e, Rectangle keepVisibleAea) {
Rectangle newArea;
if (keepVisibleAea == null) {
Point point = new Point(e.getPoint());
newArea = new Rectangle(point);
newArea.grow(X_PADDING, Y_PADDING); // pad to avoid placing the popup too close
}
else {
newArea = new Rectangle(keepVisibleAea);
}
Point point = newArea.getLocation();
SwingUtilities.convertPointToScreen(point, sourceComponent);
newArea.setLocation(point);
return newArea;
}
private void ensureSize(Dimension popupDimension) {
Dimension screenDimension = WindowUtilities.getVisibleScreenBounds().getBounds().getSize();
if (screenDimension.width < popupDimension.width) {
popupDimension.width = screenDimension.width / 2;
}
if (screenDimension.height < popupDimension.height) {
popupDimension.height = screenDimension.height / 2;
}
}
/**
* Creates a rectangle that contains both given rectangles entirely and includes padding.
* The padding allows users to mouse over the edge of the hovered area without triggering the
* popup to close.
*/
private Rectangle createMovementArea(Rectangle popupBounds, Rectangle hoverRectangle) {
Rectangle result = popupBounds.union(hoverRectangle);
return result;
}
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);
// painters.forEach(p -> glassPane.removePainter(p));
//
// glassPane.addPainter(painter);
// painters.add(painter);
}
/**
* Adjusts the given bounds to make sure that they do not cover the given location.
* <p>
* When the <tt>hoverArea</tt> 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.
*
* @param bounds The bounds to move as necessary.
* @param hoverArea The area that should not be covered by the given bounds
* @return the original bounds adjusted so that they do not cover the given <tt>hoverArea</tt>,
* if possible.
*/
private Rectangle adjustBoundsForCursorLocation(Rectangle bounds, Rectangle hoverArea) {
if (!bounds.intersects(hoverArea)) {
return bounds;
}
//==================================================================================================
// Inner Classes
//==================================================================================================
// 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;
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 bounds;
}
/**
* Creates a rectangle that contains both given rectangles entirely.
*/
private Rectangle createNeutralMotionZone(Rectangle popupBounds, Rectangle hoverRectangle) {
int newX = Math.min(hoverRectangle.x, popupBounds.x);
int newY = Math.min(hoverRectangle.y, popupBounds.y);
double hoverLowestCornerX = hoverRectangle.x + hoverRectangle.getWidth();
double popupLowestCornerX = popupBounds.x + popupBounds.getWidth();
int lowestCornerX = (int) Math.max(hoverLowestCornerX, popupLowestCornerX);
double hoverLowestCornerY = hoverRectangle.y + hoverRectangle.getHeight();
double popupLowestCornerY = popupBounds.y + popupBounds.getHeight();
int lowestCornerY = (int) Math.max(hoverLowestCornerY, popupLowestCornerY);
int width = difference(newX, lowestCornerX);
int height = difference(newY, lowestCornerY);
// add in some padding around the edges of the area, so that moving just over the edge
// of the popup will not close it (this can happen when the user sloppy-scrolls)
int padding = 25;
newX -= padding;
newY -= padding;
width += (padding * 2); // * 2 to give the padding and to compensate for the shifted x
height += (padding * 2); // * 2 to give the padding and to compensate for the shifted y
return new Rectangle(newX, newY, width, height);
}
private int difference(int value1, int value2) {
int abs1 = Math.abs(value1);
int abs2 = Math.abs(value2);
if (abs1 > abs2) {
return abs1 - abs2;
}
return abs2 - abs1;
}
// for debug
// private static List<GGlassPanePainter> painters = new ArrayList<>();
/** Paints shapes used by this class (useful for debugging) */
@SuppressWarnings("unused")
@@ -350,23 +329,27 @@ public class PopupWindow {
}
@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));
}
}
}
}

View File

@@ -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 docking.widgets.shapes;
/**
* Specifies location and metrics for {@link PopupWindowPlacer}.
*/
public enum Location {
LEFT, RIGHT, TOP, BOTTOM, CENTER;
static {
LEFT.set(false, true, RIGHT, TOP);
RIGHT.set(true, true, LEFT, BOTTOM);
TOP.set(false, false, BOTTOM, RIGHT);
BOTTOM.set(true, false, TOP, LEFT);
CENTER.set(null, null, null, null);
}
// Tri-valued value: false === Lesser, and null === Center
private Boolean isGreater;
// Tri-valued value: false === Vertical, and null === either (e.g., Center)
// Means it is a measure of horizontal (e.g., left right) as opposed to a left edge, which
// is a vertical line (the minor elements which describe location on this edge are vertical).
private Boolean isHorizontal;
private Location match;
private Location clockwiseNext;
private void set(Boolean isGreater, Boolean isHorizontal, Location match,
Location clockwiseNext) {
this.isGreater = isGreater;
this.isHorizontal = isHorizontal;
this.match = match;
this.clockwiseNext = clockwiseNext;
}
public boolean isGreater() {
return isGreater != null && isGreater;
}
public boolean isLesser() {
return isGreater != null && !isGreater;
}
public boolean isCenter() {
return isGreater == null;
}
public Location match() {
return match;
}
public Location clockwise() {
return clockwiseNext;
}
public Location counterClockwise() {
return clockwiseNext.match();
}
/**
* Assumes "this" is a major axis, and tells whether the minor axis argument is valid for
* the major value. Cannot have both major and minor be the same horizontal/vertical bearing.
* Note that {@link #CENTER} can be horizontal or vertical, so this method should not count
* this value as a bad minor value, as it also represents a good value.
* @param minor the minor value to check
* @return true if valid.
*/
public boolean validMinor(Location minor) {
return isHorizontal() && minor.isVertical() || isVertical() && minor.isHorizontal();
}
public boolean isHorizontal() {
return isHorizontal == null || isHorizontal;
}
public boolean isVertical() {
return isHorizontal == null || !isHorizontal;
}
}

View File

@@ -0,0 +1,895 @@
/* ###
* 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 docking.widgets.shapes;
import java.awt.*;
import org.apache.commons.lang3.StringUtils;
import ghidra.util.exception.AssertException;
/**
* This class places a rectangle on the boundary of an inner bounds area, such that it is not
* placed outside of an outer boundary. It takes the concept of trying to make the placement at
* the closest distance, but preferring certain sides or angles of approach in iterating a
* solution. However, we reduce this concept down to a very simple form where iteration is not
* needed because we are basing the algorithm on a geometric model that has explicit solutions
* (for example, instead of picking a starting point around the perimeter and rotating
* counter-clockwise to find a fit, or, for example, creating a grid of placements and choosing
* the one that is closest but yet has preferences on one side or another). From the geometric
* model, we can, instead, calculate the first location that will fit with a preferred boundary
* location, such as fit on the right side of the context area, near the bottom. We could have
* chosen to iterate through the areas in a counter-clockwise fashion, but by using a builder
* model, we give the user more control of the order of choice.
* For example, the user might first prefer the right side near the bottom, then the left side near
* the bottom, followed by the top near the right, and then the bottom near the right.
* <p>
* This first drawing shows the overall context of the inner bounds within an outer bounds along
* with a good placement and a bad placement that violates the outer bounds.
* <pre>
*
* +-----------------------------------------------+
* | outer |
* | |
* | |
* | +------------------+ |
* | | good | |
* | | placement | |
* | | | |
* | +------------------+---------+ |
* | | | |
* | | inner | |
* | | | |
* | +---------+------------------+
* | | bad | |
* | | placement |
* +-----------------------------------+-----------+ |
* +------------------+
*
* </pre>
*
* The next two drawings show the LEFT and RIGHT edges with nominal locations of TOP, CENTER, and
* BOTTOM placements and the TOP and BOTTOM edges with nominal location of LEFT, CENTER, and
* RIGHT placements. There are a total of eight of these locations ("cells") around the inner
* bounds.
* <pre>
*
* LEFT RIGHT
* +---------------+ +---------------+
* | | | |
* | TOP | | TOP |
* | | | |
* +---------------X---------------X---------------+
* | | | |
* | CENTER X inner X CENTER |
* | | | |
* +---------------X---------------X---------------+
* | | | |
* | BOTTOM | | BOTTOM |
* | | | |
* +---------------+ +---------------+
*
*
* +---------------+---------------+---------------+
* | | | |
* | LEFT | CENTER | RIGHT | TOP
* | | | |
* +---------------X-------X-------X---------------+
* | |
* | inner |
* | |
* +---------------X-------X-------X---------------+
* | | | |
* | LEFT | CENTER | RIGHT | BOTTOM
* | | | |
* +---------------+---------------+---------------+
*
* </pre>
* <p>
*
* These cells are shown in their nominal placement locations (where they touch the inner bounds,
* marked with an X). However we will shift these locations by particular amounts so that these
* locations still fit within the outer bounds. For instance, if we allow the BOTTOM cell
* on the LEFT edge to be shifted up far enough such that it fits the lower edge of the outer
* bounds, we limit this shift if it reaches the nominal placement of another specified cell
* (CENTER or TOP) on that edge. If a solution is not found before the limit is reached, the
* placement fails.
* <p>
* If the chosen cell is a CENTER cell, then it could shift up or down, depending on the
* circumstances and the parameters applied.
* <p>
* These placements and shifts are controlled by specifying the <B>major</B> and <B>minorBegin</B>
* and <B>minorEnd</B> {@link Location Locations}. The major Location specifies the <B>edge</B>
* for an {@link EdgePopupPlacer} and the minorBegin Location specifies the placement <B>cell</B>
* on this edge and the minorEnds specifies the last cell (amount of shift allowed), starting
* from the minorBegin Location. For a CENTER minorBeing Location, the minorEnd cell may be
* any of the three allowed Locations on that major edge as well as null, representing that a
* shift is allowed in either direction. When the minorEnd Location is set to the minorBegin
* Location, then no shift is permitted.
* <p>
* Combinations of these placement attempts can be put together to create more complex strategies.
* See {@link PopupWindowPlacerBuilder} for examples of these.
* <p>
* There are also {@link LeastOverlapCornerPopupWindowPlacer} and
* {@link ThrowsAssertExceptionPlacer}, for instance, that do not follow the same cell scheme.
* The first of these tries to make the placement at each of the corners of the inner
* bounds, but shifts these placements to fit the outer bounds in such a way that the inner
* bounds area may be occluded. The placement on the corner which overlaps the least amount of
* the inner bounds area is chosen. The second of these placers automatically throws an
* {@link AssertException}. It is intended to be used in a builder model in which a sequence of
* placement attempts are made until good solution is found or until a null value is returned.
* This last placer, when chosen, serves as an assert condition, which is helpful
* in circumstances where the developer believes such an assertion is not possible,
* such as when allowing an overlapping placement solution.
*
* @see PopupWindowPlacerBuilder
*/
public abstract class PopupWindowPlacer {
protected Location major;
protected Location minorBegin;
protected Location minorEnd;
private PopupWindowPlacer next = null;
/**
* Constructor only for classes that do not use placement preferences
*/
public PopupWindowPlacer() {
// Only for implementations that do not use placement preferences
}
/**
* Constructor only for classes that specify major edge and minor begin and end location
* on that edge.
* @param major edge
* @param minorBegin start location on edge
* @param minorEnd end location on edge
*
* @see PopupWindowPlacerBuilder
*/
public PopupWindowPlacer(Location major, Location minorBegin, Location minorEnd) {
if (major == Location.CENTER) {
throw new IllegalArgumentException("Cannot use " + major + " for major edge.");
}
if (!major.validMinor(minorBegin)) {
throw new IllegalArgumentException(
"Invalid minor location for " + major + " edge: " + minorBegin);
}
if (!major.validMinor(minorEnd)) {
throw new IllegalArgumentException(
"Invalid minor location for " + major + " edge: " + minorEnd);
}
this.major = major;
this.minorBegin = minorBegin;
this.minorEnd = minorEnd;
}
void setNext(PopupWindowPlacer next) {
this.next = next;
}
/**
* Returns the placement Rectangle of toBePlaced Dimension for this PopupWindowPlacer. If it
* cannot find a solution, it tries the {@link #next} PopupWindowPlacer and so forth until
* there are no others available, upon which null is returned if there is no solution.
* @param toBePlaced the Dimension
* @param innerBounds the inner bounds Rectangle
* @param outerBounds the out bounds in which the final result must fit
* @return the placement Rectangle or null if extends outside the outerBounds
*/
public Rectangle getPlacement(Dimension toBePlaced, Rectangle innerBounds,
Rectangle outerBounds) {
Rectangle myPlacement = getMyPlacement(toBePlaced, innerBounds, outerBounds);
//Msg.info(this, debugDump(myPlacement));
if (myPlacement != null) {
return myPlacement;
}
if (next != null) {
return next.getPlacement(toBePlaced, innerBounds, outerBounds);
}
return null;
}
protected abstract Rectangle getMyPlacement(Dimension toBePlaced, Rectangle innerBounds,
Rectangle outerBounds);
/**
* Returns a Rectangle solution for the placement of a toBePlaced Dimension.
* <p>
* When dealing with solutions for the top or bottom edge, we are considering <B>vertical</B>
* to be the major axis with y/height values representing that axis, and <B>horizontal</B>
* to be the minor axis with x/width values representing that axis. When dealing with
* solutions for the left and right edge, these major and minor axes are switched.
*
* @param result the new instance of the resulting class type to be returned
* @param toBePlaced the wrapped toBePlaced Dimension
* @param context the wrapped context Rectangle
* @param outer the wrapped outer boundsRectangle
* @return the resultant wrapped Rectangle
*/
protected PositionableRectangle getPlacement(PositionableRectangle result,
PositionableDimension toBePlaced, PositionableRectangle context,
PositionableRectangle outer) {
// Test major axis edge
int placementMajorCoordinate;
if (major.isLesser()) {
placementMajorCoordinate = getLesserLocation(context.getMajorCoordinate(),
context.getMajorLength(), toBePlaced.getMajorLength());
int shiftedPlacement =
getLesserBoundedLocation(placementMajorCoordinate, outer.getMajorCoordinate());
if (placementMajorCoordinate < shiftedPlacement) {
return null; // no solution on edge
}
}
else if (major.isGreater()) {
placementMajorCoordinate = getGreaterLocation(context.getMajorCoordinate(),
context.getMajorLength(), toBePlaced.getMajorLength());
int shiftedPlacement = getGreaterBoundedLocation(placementMajorCoordinate,
toBePlaced.getMajorLength(), outer.getMajorCoordinate(), outer.getMajorLength());
if (placementMajorCoordinate > shiftedPlacement) {
return null; // no solution on edge
}
}
else {
throw new AssertException("Should not get here.");
}
// Find placement on the edge using minor axis
Integer placementMinorCoordinate =
getPlacement(toBePlaced.getMinorLength(), context.getMinorCoordinate(),
context.getMinorLength(), outer.getMinorCoordinate(), outer.getMinorLength());
if (placementMinorCoordinate == null) {
return null; // no solution on edge
}
result.set(placementMajorCoordinate, placementMinorCoordinate, toBePlaced);
return result;
}
/**
* With all inputs on a line (one-dimensional), returns the placement for the minor axis.
* In other words, this algorithm is used for both conditions: the major axis being horizontal
* and the minor axis being vertical; the major axis being vertical, and the minor axis being
* horizontal. These two situations are independent, but the same algorithm is used.
* <p>
* <B>Algorithm Design</B><p>
* Note: smaller values are up and bigger values are down, in the presentation below.
* <p>
* In trying to allay some confusion (yes it can be confusing), note that for any given major
* axis (say horizontal), this axis can portray values that are further right or further left.
* This is why the left edge and right edge are noted by horizontal axes values... one
* intersects the horizontal axis further to the left and the other intersects the axis
* further to the right.
* <p>
* The location of placements on the left or right edges, however are noted by vertical axis
* values, with TOP having a lesser value and bottom having a greater value. These locations
* specified by the minor dimension, and are the "one dimension" that is the subject of this
* placement algorithm.
* <p>
* The scenario with top and bottom edge reverses the major dimension to be vertical and the
* minor dimension to be horizontal.
* <p>
* Keeping with the original right edge scenario begun above, we are trying to find a minor
* axis placement on the right (major) edge. For this placement, one can refer to
* documentation elsewhere in this class, but essentially, we are trying to place a popup
* area against a context rectangle without exceeding the outer bounds (screen) rectangle. But,
* again, we are only considering the placement against the right edge already chosen and only
* trying to fit in the vertical dimension against this edge. Thus, this algorithm only needs
* values for this one dimension. These are the length of the placement area in this one
* dimension, and both the location and lengths of the context and outer bounds rectangles
* for this one dimension.
* <p>
* The algorithm considers three main locations (cells) on this minor axis. When the minor
* axis is vertical (which is our current scenario), they are TOP, CENTER, and BOTTOM. When
* the minor axis is horizontal, they are LEFT, CENTER, and RIGHT. These locations are
* nominal, but are also allowed to be shifted so that the placement fits within the outer
* bounds. Thus, we have five key values, in which three have fixed relative placements
* (when using <B>positive</B> lengths):
* <pre>
* <B>lesserLocation</B> (nominal placement on TOP or LEFT)
* &le;
* <B>centerLocation</B> (nominal placement such that the center of the context rectangle
* and the center of the popup area align with each other)
* &le;
* <B>greaterLocation</B> (nominal placement on BOTTOM or RIGHT)
* </pre>
* and these two can be found at various placements amongst the other three:
* <pre>
* <B>lesserBoundedLocation &ge; lesserLocation</B> (lesserLocation shifted so TOP or
* LEFT fits outer bounds)
* <B>greaterBoundedLocation &le; greaterLocation</B> (lesserLocation shifted BOTTOM or
* RIGHT fits outer bounds)
* </pre>
* Note that with an ill-constructed scenario, as shown here, we return <B>no solution</B>:
* <pre>
* <B>greaterBoundedLocation</B>
* &lt;
* <B>lesserBoundedLocation</B>
* </pre>
* Given a better-constructed scenario, the <B>lesserBoundedLocation</B> and
* <B>greaterBoundedLocation</B> values can fall between the other three values at the following
* possible locations:
* <pre>
* <B>lesserLocation</B>
* <B>&rarr;</B> <B>lesserBoundedLocation</B> (&ge; <B>lesserLocation</B>)
* <B>&rarr;</B> <B>greaterBoundedLocation</B> (&le; <B>greaterLocation</B>)
* <B>centerLocation</B>
* <B>&rarr;</B> <B>lesserBoundedLocation</B> (&ge; <B>lesserLocation</B>)
* <B>&rarr;</B> <B>greaterBoundedLocation</B> (&le; <B>greaterLocation</B>)
* <B>greaterLocation</B>
* </pre>
* These layout possibilities can be broken down into three possibilities...
* <pre>
* <B>lesserLocation</B>
* <B>&rarr;</B> <B>lesserBoundedLocation</B> (&ge; <B>lesserLocation</B>)
* <B>&rarr;</B> <B>greaterBoundedLocation</B>
* <B>centerLocation</B>
* <B>greaterLocation</B>
* ----------
* if start is LESSER
* if end is LESSER and lesserBoundedLocation != lesserLocation
* no solution
* else
* solution is lesserBoundedLocation
* else
* if end is LESSER
* solution is greaterBoundedLocation
* else
* no solution
* </pre>
* or
* <pre>
* <B>lesserLocation</B>
* <B>&rarr;</B> <B>lesserBoundedLocation</B> (&ge; <B>lesserLocation</B>)
* <B>centerLocation</B>
* <B>&rarr;</B> <B>greaterBoundedLocation</B> (&le; <B>greaterLocation</B>)
* <B>greaterLocation</B>
* ----------
* if start is GREATER
* if end is GREATER and greaterBoundedLocation != greaterLocation
* no solution
* else
* solution is greaterBoundedLocation
* else if start is LESSER
* if end is LESSER and lesserBoundedLocation != lesserLocation
* no solution
* else
* solution is lesserBoundedLocation
* else
* solution is centerLocation
* </pre>
* or
* <pre>
* <B>lesserLocation</B>
* <B>centerLocation</B>
* <B>&rarr;</B> <B>lesserBoundedLocation</B>
* <B>&rarr;</B> <B>greaterBoundedLocation</B> (&le; <B>greaterLocation</B>)
* <B>greaterLocation</B>
* ----------
* if start is GREATER
* if end is GREATER and greaterBoundedLocation != greaterLocation
* no solution
* else
* solution is greaterBoundedLocation
* else
* if end is GREATER
* solution is lesserBoundedLocation
* else
* no solution
* </pre>
* The algorithm breaks down into these scenarios and presents the solution as required.
* @param placementLength the length of the placement Dimension on the line
* @param contextLocation location of the context Rectangle on the line
* @param contextLength the length of the context Rectangle Dimension on the line
* @param boundLocation location of the outer bounds Rectangle on the line
* @param boundLength the length of the outer bounds Rectangle Dimension on the line
* @return the resultant location on the line
*/
private Integer getPlacement(int placementLength, int contextLocation, int contextLength,
int boundLocation, int boundLength) {
int lesserLocation = getLesserLocation(contextLocation, contextLength, placementLength);
int lesserBoundedLocation = getLesserBoundedLocation(lesserLocation, boundLocation);
int greaterLocation = getGreaterLocation(contextLocation, contextLength, placementLength);
int greaterBoundedLocation =
getGreaterBoundedLocation(greaterLocation, placementLength, boundLocation, boundLength);
if (greaterBoundedLocation < lesserBoundedLocation) {
return null; // no solution
}
int centerLocation = getCenterLocation(contextLocation, contextLength, placementLength);
if (greaterBoundedLocation < centerLocation) {
return getSolutionWhenGreaterBoundedLessThanCenter(lesserLocation,
lesserBoundedLocation, greaterBoundedLocation);
}
if (lesserBoundedLocation > centerLocation) {
return getSolutionWhenLesserBoundedGreaterThanCenter(lesserBoundedLocation,
greaterBoundedLocation, greaterLocation);
}
return getSolutionWhenCenterBounded(lesserLocation, lesserBoundedLocation, centerLocation,
greaterBoundedLocation, greaterLocation);
}
private Integer getSolutionWhenGreaterBoundedLessThanCenter(int lesserLocation,
int lesserBoundedLocation, int greaterBoundedLocation) {
if (minorBegin.isLesser()) {
if (minorEnd.isLesser() && lesserLocation != lesserBoundedLocation) {
return null; // no solution
}
return lesserBoundedLocation;
}
if (minorEnd.isLesser()) {
return greaterBoundedLocation;
}
return null; // no solution
}
private Integer getSolutionWhenLesserBoundedGreaterThanCenter(int lesserBoundedLocation,
int greaterBoundedLocation, int greaterLocation) {
if (minorBegin.isGreater()) {
if (minorEnd.isGreater() && greaterLocation != greaterBoundedLocation) {
return null; // no solution
}
return greaterBoundedLocation;
}
if (minorEnd.isGreater()) {
return lesserBoundedLocation;
}
return null; // no solution
}
private Integer getSolutionWhenCenterBounded(int lesserLocation, int lesserBoundedLocation,
int centerLocation, int greaterBoundedLocation, int greaterLocation) {
if (minorBegin.isGreater()) {
if (minorEnd.isGreater() && greaterLocation != greaterBoundedLocation) {
return null; // no solution
}
return greaterBoundedLocation;
}
else if (minorBegin.isLesser()) {
if (minorEnd.isLesser() && lesserLocation != lesserBoundedLocation) {
return null; // no solution
}
return lesserBoundedLocation;
}
return centerLocation;
}
/**
* With all inputs on a line (one-dimensional), returns a location that is shifted enough from
* the placementLocation such that the greater end of bounds specified by boundLocation
* is not exceeded (i.e., the new location is not bigger than {@code #boundLocation}).
*
* @param placementLocation starting location that gets shifted
* @param placementLength the length of the to-be-placed dimension on the (one-dimensional)
* line
* @param boundLocation the bounds on the line that must not be exceeded to the greater side
* @param boundLength the length of the outer bounds dimension on the (one-dimensional) line
* @return the shifted result
*/
protected int getGreaterBoundedLocation(int placementLocation, int placementLength,
int boundLocation, int boundLength) {
return Integer.min(placementLocation, boundLocation + boundLength - placementLength);
}
/**
* With all inputs on a line (one-dimensional), returns a location that is shifted enough from
* the placementLocation such that the lesser end of bounds specified by boundLocation
* is not exceeded (i.e., the new location is not smaller than boundLocation).
*
* @param placementLocation starting location that gets shifted
* @param boundLocation the bounds on the line that must not be exceeded to the lesser side
* @return the shifted result
*/
protected int getLesserBoundedLocation(int placementLocation, int boundLocation) {
return Integer.max(placementLocation, boundLocation);
}
/**
* Returns the placement on a line (one-dimensional) on the greater end of the context area.
*
* @param contextLocation the context location on the line
* @param contextLength the context length on the line
* @param placementLength the length of the to-be-place dimension on that line
* @return the resultant placement on the line
*/
protected int getGreaterLocation(int contextLocation, int contextLength, int placementLength) {
return contextLocation + contextLength;
}
/**
* Returns the placement on a line (one-dimensional) on the lesser end of the context area.
*
* @param contextLocation the context location on the line
* @param contextLength the context length on the line
* @param placementLength the length of the to-be-place dimension on that line
* @return the resultant placement on the line
*/
protected int getLesserLocation(int contextLocation, int contextLength, int placementLength) {
return contextLocation - placementLength;
}
/**
* Determines the placementLocation such that the midpoint of the context and the midpoint
* of the placement are at the same point. Location and Length can either be an x value and
* width or a y value and height.
*
* @param contextLocation the x or y value of the context, depending on if we are doing the
* horizontal or vertical midpoint
* @param contextLength the corresponding width (if dealing with x/horizontal midpoint) or
* height (if dealing with y/vertical midpoint)
* @param placementLength the corresponding height or width of the placement
* @return the placement location (again x or y value)
*/
protected int getCenterLocation(int contextLocation, int contextLength, int placementLength) {
return contextLocation + (contextLength - placementLength) / 2;
}
/** Dumps some debug output about the current class and its placement result*/
@SuppressWarnings("unused")
private String debugDump(Rectangle placement) {
return String.format("%s: %s(%s,%s)... placement %s", getClass().getSimpleName(), major,
minorBegin, minorEnd, dumpRectangle(placement));
}
/** Dumps a simple Rectangle output */
private String dumpRectangle(Rectangle r) {
if (r == null) {
return "null";
}
return String.format("[x=%d,y=%d,width=%d,height=%d]", r.x, r.y, r.width, r.height);
}
@Override
public String toString() {
String name = getClass().getSimpleName();
String specificName = name.replace(PopupWindowPlacer.class.getSimpleName(), "");
String[] words = StringUtils.splitByCharacterTypeCamelCase(specificName);
return StringUtils.join(words, ' ');
}
//==================================================================================================
// Placer Classes
//==================================================================================================
/**
* Placer that attempts a placement on the <code>major</code> edge of the inner bounds, with
* <code>minorBegin</code> specifying the preferred cell location at which to start the
* placement attempt and <code>minorEnd</code> specifying that limit on the amount of shift
* that is made in an attempt to make the placement fit within the outer bounds. The inner
* bounds is not allowed to be violated.
*/
static class EdgePopupPlacer extends PopupWindowPlacer {
public EdgePopupPlacer(Location major, Location minorBegin, Location minorEnd) {
super(major, minorBegin, minorEnd);
}
@Override
public Rectangle getMyPlacement(Dimension toBePlaced, Rectangle context, Rectangle outer) {
if (major.isHorizontal()) {
return getPlacement(new HorizontalMajorRectangle(),
new HorizontalMajorDimension(toBePlaced), new HorizontalMajorRectangle(context),
new HorizontalMajorRectangle(outer));
}
return getPlacement(new VerticalMajorRectangle(),
new VerticalMajorDimension(toBePlaced), new VerticalMajorRectangle(context),
new VerticalMajorRectangle(outer));
}
}
/**
* Placer picks corner with toBePlaced as the least overlap with innerBounds. In the case of a
* tie, the tie-breaker is first in this order: Bottom Right, Bottom Left, Top Right, Top Left.
*/
static class LeastOverlapCornerPopupWindowPlacer extends PopupWindowPlacer {
public LeastOverlapCornerPopupWindowPlacer() {
super();
}
@Override
public Rectangle getMyPlacement(Dimension toBePlaced, Rectangle context, Rectangle outer) {
Rectangle bestRectangle = null;
int bestArea = Integer.MAX_VALUE;
Rectangle rectangle;
Rectangle intersection;
int area;
int top = getLesserLocation(context.y, context.height, toBePlaced.height);
int bottom = getGreaterLocation(context.y, context.height, toBePlaced.height);
int left = getLesserLocation(context.x, context.width, toBePlaced.width);
int right = getGreaterLocation(context.x, context.width, toBePlaced.width);
int topShifted = getLesserBoundedLocation(top, outer.y);
int bottomShifted =
getGreaterBoundedLocation(bottom, toBePlaced.height, outer.y, outer.height);
int leftShifted = getLesserBoundedLocation(left, outer.x);
int rightShifted =
getGreaterBoundedLocation(right, toBePlaced.width, outer.x, outer.width);
if (bottomShifted < topShifted || rightShifted < leftShifted) {
return null; // no solution fits within outer bounds
}
// Bottom Right
rectangle = new Rectangle(new Point(rightShifted, bottomShifted), toBePlaced);
intersection = rectangle.intersection(context);
area = intersection.width * intersection.height;
if (area < bestArea) {
bestArea = area;
bestRectangle = rectangle;
}
// Bottom Left
rectangle = new Rectangle(new Point(leftShifted, bottomShifted), toBePlaced);
intersection = rectangle.intersection(context);
area = intersection.width * intersection.height;
if (area < bestArea) {
bestArea = area;
bestRectangle = rectangle;
}
// Top Right
rectangle = new Rectangle(new Point(rightShifted, topShifted), toBePlaced);
intersection = rectangle.intersection(context);
area = intersection.width * intersection.height;
if (area < bestArea) {
bestArea = area;
bestRectangle = rectangle;
}
// Top Left
rectangle = new Rectangle(new Point(leftShifted, topShifted), toBePlaced);
intersection = rectangle.intersection(context);
area = intersection.width * intersection.height;
if (area < bestArea) {
bestArea = area;
bestRectangle = rectangle;
}
return bestRectangle;
}
}
/**
* Set the next PopupWindowPlacer that throws an AssertException because no solution has
* been found by the time this placer is tried. This is intended to be used when the client
* has already guaranteed that there is a solution (i.e., this placer is been used and the
* pop-up area will fit within the outer bounds).
*/
static class ThrowsAssertExceptionPlacer extends PopupWindowPlacer {
@Override
public Rectangle getMyPlacement(Dimension toBePlaced, Rectangle innerBounds,
Rectangle outerBounds) {
throw new AssertException("Unexpected popup placement error.");
}
}
//==================================================================================================
// Size and Shape Classes
//==================================================================================================
private static abstract class PositionableDimension extends Dimension {
public PositionableDimension(Dimension dimension) {
super(dimension);
}
abstract int getMajorLength();
abstract void setMajorLength(int length);
abstract int getMinorLength();
abstract void setMinorLength(int length);
}
private static class HorizontalMajorDimension extends PositionableDimension {
public HorizontalMajorDimension(Dimension dimension) {
super(dimension);
}
@Override
int getMajorLength() {
return this.width;
}
@Override
void setMajorLength(int length) {
this.width = length;
}
@Override
int getMinorLength() {
return this.height;
}
@Override
void setMinorLength(int length) {
this.height = length;
}
}
private static class VerticalMajorDimension extends PositionableDimension {
public VerticalMajorDimension(Dimension dimension) {
super(dimension);
}
@Override
int getMajorLength() {
return this.height;
}
@Override
void setMajorLength(int length) {
this.height = length;
}
@Override
int getMinorLength() {
return this.width;
}
@Override
void setMinorLength(int length) {
this.width = length;
}
}
private static abstract class PositionableRectangle extends Rectangle {
PositionableRectangle() {
super();
}
public PositionableRectangle(Rectangle rectangle) {
super(rectangle);
}
public void set(int majorCoordinate, int minorCoordinate, PositionableDimension dimension) {
setMajorCoordinate(majorCoordinate);
setMinorCoordinate(minorCoordinate);
setSize(dimension);
}
abstract int getMajorCoordinate();
abstract void setMajorCoordinate(int coordinate);
abstract int getMinorCoordinate();
abstract void setMinorCoordinate(int coordinate);
abstract int getMajorLength();
abstract void setMajorLength(int length);
abstract int getMinorLength();
abstract void setMinorLength(int length);
}
private static class HorizontalMajorRectangle extends PositionableRectangle {
HorizontalMajorRectangle() {
super();
}
public HorizontalMajorRectangle(Rectangle rectangle) {
super(rectangle);
}
@Override
int getMajorCoordinate() {
return this.x;
}
@Override
void setMajorCoordinate(int coordinate) {
this.x = coordinate;
}
@Override
int getMinorCoordinate() {
return this.y;
}
@Override
void setMinorCoordinate(int coordinate) {
this.y = coordinate;
}
@Override
int getMajorLength() {
return this.width;
}
@Override
void setMajorLength(int length) {
this.width = length;
}
@Override
int getMinorLength() {
return this.height;
}
@Override
void setMinorLength(int length) {
this.height = length;
}
}
private static class VerticalMajorRectangle extends PositionableRectangle {
VerticalMajorRectangle() {
super();
}
public VerticalMajorRectangle(Rectangle rectangle) {
super(rectangle);
}
@Override
int getMajorCoordinate() {
return this.y;
}
@Override
void setMajorCoordinate(int coordinate) {
this.y = coordinate;
}
@Override
int getMinorCoordinate() {
return this.x;
}
@Override
void setMinorCoordinate(int coordinate) {
this.x = coordinate;
}
@Override
int getMajorLength() {
return this.height;
}
@Override
void setMajorLength(int length) {
this.height = length;
}
@Override
int getMinorLength() {
return this.width;
}
@Override
void setMinorLength(int length) {
this.width = length;
}
}
}

View File

@@ -0,0 +1,429 @@
/* ###
* 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 docking.widgets.shapes;
import docking.widgets.shapes.PopupWindowPlacer.*;
/**
* This class builds a PopWindowPlacer that can have subsequent PopWindowPlacers.
* <p>
* General categories of placers available are <B>edge</B> placers, <B>overlapped-corner</B>
* placers, and a clean-up <B>assert</B> placer. Additionally, there are <B>rotational</B> placers
* that are composed of edge placers.
* <p>
* <BR>
* <BR>
*
* <H1>Edge Placers</H1>
*
* <p>
* The <B>edge</B> placers are the leftEdge, rightEdge, topEdge, and bottomEdge methods that take
* Location arguments that one can think of as "cells" for optimal placement, but which have some
* flexibility in making the placement. One such cell is the TOP Location of the rightEdge,
* specified by <code>rightEdge(Location.TOP)</code>. If the placement does not quite fit this
* cell because the optimal placement extend above the top of the screen, the placement may be
* shifted down by a allowed amount so that it still fits. If more than the allowed amount is
* needed, the placement fails.
* <p>
* Each edge placer takes a variable number of Location arguments. These arguments work in the
* same way for each method, though some arguments are not valid for some edges; for instance,
* <code>Location.TOP</code> is only valid for left and right edges.
* <p>
*
* <H2>Two or More Location Arguments</H2>
*
* <p>
* When two or more arguments are used, the first argument specifies the nominal placement cell
* and the second argument specifies how far the solution is allowed to shift. If a solution is
* not found and if there are more than two arguments, another placement attempt is made where
* the second argument specifies the nominal placement cell and the third argument specifies how
* far the solution is allowed to shift. To specify a "no-shift" solution, one specifies the same
* placement cell twice (e.g., <code>rightEdge(Location.TOP, Location.TOP)</code>).
* <p>
*
* <H2>One Location Argument</H2>
*
* <p>
* When one argument is used, the solution is the same as when two arguments are specified except
* that the second argument is automatically set to the nearest neighboring cell. Thus,
* <code>rightEdge(Location.TOP)</code> is the same as
* <code>rightEdge(Location.TOP, Location.CENTER)</code>. When the single argument is
* <code>Location.CENTER</code>, two attempts are built, the first being the BOTTOM or RIGHT cell
* and the second being the TOP or LEFT cell.
* <p>
*
* <H2>No Arguments</H2>
*
* <p>
* When no arguments are specified, two arguments to the underlying placer are automatically set
* to BOTTOM or RIGHT for the first and TOP or LEFT for the second.
* <p>
*
* <H2>Examples</H2>
*
* <p>
* Builds a placer that first attempts a placement at the bottom of the right edge with no
* shift, then tries the top of the right edge with no shift, then top center with no shift:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .rightEdge(Location.BOTTOM,Location.BOTTOM)
* .rightEdge(Location.TOP, Location.TOP)
* .topEdge(Location.CENTER, Location.CENTER)
* .build();</pre>
* Builds a placer that attempts a placement on the right edge from bottom to top, followed by
* the top edge from center to right, then center to left:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .rightEdge()
* .topEdge(Location.CENTER);
* .build();</pre>
* <p>
* <BR>
* <BR>
*
* <H1>Rotational Placers</H1>
*
* <p>
* There are clockwise and counter-clockwise rotational placers that built up from edge placers.
* These are:
* <pre>
* rotateClockwise(Location major, Location minor)
* rotateCounterClockwise(Location major, Location minor)
* thenRotateClockwise()
* thenRotateCounterClockwise()</pre>
* The first two of these take two Location arguments the specify the starting cell. For instance,
* <code>rotateClockwise(Location.BOTTOM, Location.RIGHT)</code>. This specifies a set of edge
* placers that attempt placement starting from the specified cell, and making attempt in a
* clockwise fashion until the starting cell is revisited, at which time the attempt fails if a
* viable placement has not been found. The <code>rotateCounterClockwise</code> placer works the
* same, but in a counter-clockwise fashion. The <code>thenRotateClockwise</code> and
* <code>thenRotateCounterClockwise</code> placers are the same as the previous two placers
* except that they start at the "beginning" cell where the most previous placer had left off. If
* there was not a previous placer, then the BOTTOM RIGHT cell is chosen as the starting cell.
* <p>
* <BR>
* <BR>
*
* <H1>Overlapping Corner Placer</H1>
*
* <p>
* There is one corner placer, <code>leastOverlapCorner()</code>. This placer tries to make a
* placement at each of the corners of the context area and shifts into the context region as much
* as necessary to fit the screen bounds. The corner that overlaps the context area the least is
* chosen as the solution placement corner. In case of a tie (e.g., no overlap on some corners),
* the placement order chosen in this preference order: bottom right, bottom left, top right, and
* top left. Unless ill-constructed (sized of context area, screen, and pop-up dimension), this
* placer should always find a solution.
* <p>
* <BR>
* <BR>
*
* <H1>Assert Placer</H1>
*
* <p>
* The <code>throwsAssertException()</code> placer is available, which automatically throws an
* AssertException. This placer is only intended to be used by the client in such as case when
* it is believed that a placement should have already been found, such as after the
* <code>leastOverlapCorner()</code> placer. This just throws an exception instead of returning
* the <code>null</code> return value that would be returned from previous placement attempts.
* <p>
* <BR>
* <BR>
*
* <H1>Composite Placer</H1>
*
* <p>
* Builds a placer that first attempts a placement at the right edge from bottom to top, then
* left edge from bottom to top, then top edge from right to left, then bottom edge from right to
* left, followed by a least-overlap-corner solution, followed by a failure assert:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .rightEdge()
* .leftEdge()
* .topEdge()
* .bottomEdge()
* .leastOverlapCorner()
* .throwsAssertException()
* .build();</pre>
* <p>
* Builds a placer that first attempts each of the four major corners in a specific order, with no
* shifting, followed by an assertion failure:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .rightEdge(Location.BOTTOM, Location.BOTTOM)
* .leftEdge(Location.TOP, Location.TOP)
* .rightEdge(Location.TOP, Location.TOP)
* .leftEdge(Location.BOTTOM, Location.BOTTOM)
* .throwsAssertException()
* .build();</pre>
* <p>
* Builds a placer that attempt to make a placement at the bottom right corner, first shifting up
* to the center location then shifting left to the center location, then failing only with a
* null return:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .rightEdge(Location.BOTTOM)
* .bottomEdge(Location.RIGHT)
* .build();</pre>
* <p>
* Builds a placer that attempts a placement at the top, left corner, the tries to make a placement
* in a clockwise fashion, followed by a failure assert:
* <pre>
* PopupWindowPlacer placer =
* new PopupWindowPlacerBuilder()
* .topEdge(Location.LEFT, Location.LEFT)
* .thenRotateClockwise()
* .throwsAssertException()
* .build();</pre>
*
* @see PopupWindowPlacer
*/
public class PopupWindowPlacerBuilder {
private PopupWindowPlacer head = null;
private PopupWindowPlacer current = null;
/**
* Builds the final PopupWindowPlacer.
* @return the PopupWindowPlacer
*/
public PopupWindowPlacer build() {
return head;
}
private void add(PopupWindowPlacer next) {
if (current == null) {
current = next;
head = current;
}
else {
current.setNext(next);
current = next;
}
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement at the right
* edge of the inner bounds (context) without exceeding outer bounds (screen), using
* an ordered, preferred placements on that edge. Invalid values will error.
* @param minors the ordered, preferred placements on the edge. If not specified, goes from
* greater-valued end of the edge to the lesser-valued end of the edge.
* @return this builder
*/
public PopupWindowPlacerBuilder rightEdge(Location... minors) {
return edge(Location.RIGHT, minors);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement at the left
* edge of the inner bounds (context) without exceeding outer bounds (screen), using
* an ordered, preferred placements on that edge. Invalid values will error.
* @param minors the ordered, preferred placements on the edge. If not specified, goes from
* greater-valued end of the edge to the lesser-valued end of the edge.
* @return this builder
*/
public PopupWindowPlacerBuilder leftEdge(Location... minors) {
return edge(Location.LEFT, minors);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement at the bottom
* edge of the inner bounds (context) without exceeding outer bounds (screen), using
* an ordered, preferred placements on that edge. Invalid values will error.
* @param minors the ordered, preferred placements on the edge. If not specified, goes from
* greater-valued end of the edge to the lesser-valued end of the edge.
* @return this builder
*/
public PopupWindowPlacerBuilder bottomEdge(Location... minors) {
return edge(Location.BOTTOM, minors);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement at the top
* edge of the inner bounds (context) without exceeding outer bounds (screen), using
* an ordered, preferred placements on that edge. Invalid values will error.
* @param minors the ordered, preferred placements on the edge. If not specified, goes from
* greater-valued end of the edge to the lesser-valued end of the edge.
* @return this builder
*/
public PopupWindowPlacerBuilder topEdge(Location... minors) {
return edge(Location.TOP, minors);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement on the major
* edge of the inner bounds (context) without exceeding outer bounds (screen), using
* an ordered, preferred placements on that edge. Invalid values will error.
* @param major the major edge of the context area
* @param minors the ordered, preferred placements on the edge. If not specified, goes from
* greater-valued end of the edge to the lesser-valued end of the edge.
* @return this builder
*/
public PopupWindowPlacerBuilder edge(Location major, Location... minors) {
if (minors.length > 3) {
throw new IllegalArgumentException("Too many preferred Locations: " + minors);
}
for (Location minor : minors) {
if (!major.validMinor(minor)) {
throw new IllegalArgumentException(
"Preferred Location " + minor + " is not valid for " + major + " edge.");
}
}
if (minors.length == 0) {
// We are defaulting this as greater to lesser
if (major.isHorizontal()) {
add(new EdgePopupPlacer(major, Location.BOTTOM, Location.TOP));
}
else {
add(new EdgePopupPlacer(major, Location.RIGHT, Location.LEFT));
}
}
else if (minors.length == 1) {
if (minors[0] == Location.CENTER) {
// Trying center to greater and then center to lesser.
if (major.isHorizontal()) {
add(new EdgePopupPlacer(major, minors[0], Location.BOTTOM));
add(new EdgePopupPlacer(major, minors[0], Location.TOP));
}
else {
add(new EdgePopupPlacer(major, minors[0], Location.RIGHT));
add(new EdgePopupPlacer(major, minors[0], Location.LEFT));
}
}
else {
// Only looking from greater/lesser to the the center.
add(new EdgePopupPlacer(major, minors[0], Location.CENTER));
}
}
else { // Since we tested minors.length > 3 above, then we know we must have 2 or 3
for (int i = 0; i < minors.length - 1; i++) {
add(new EdgePopupPlacer(major, minors[i], minors[i + 1]));
}
}
return this;
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement by starting at
* the last-used {@code majorBegin} and {@code minorBegin} and continues clockwise
* to find a solution. If there was no last-used location set, then BOTTOM, RIGHT is used.
* @return this builder
*/
public PopupWindowPlacerBuilder thenRotateClockwise() {
if (current == null) {
return rotateClockwise(Location.BOTTOM, Location.RIGHT);
}
return rotateClockwise(current.major, current.minorBegin);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement by starting at
* a point specified by {@code majorBegin} and {@code minorBegin} and continues
* clockwise to find a solution.
* @param majorBegin the major coordinate location of the starting point
* @param minorBegin the minor coordinate location of the starting point
* @return this builder
*/
public PopupWindowPlacerBuilder rotateClockwise(Location majorBegin, Location minorBegin) {
Location major = majorBegin;
Location minor = minorBegin;
do {
add(new EdgePopupPlacer(major, minor, major.clockwise()));
minor = major;
major = major.clockwise();
}
while (major != majorBegin);
if (minor != minorBegin) {
// Does remaining portion of initial edge, but repeats first location.
// So if starting at BOTTOM CENTER, will repeat that location in the last partial edge
add(new EdgePopupPlacer(major, minor, minorBegin));
}
return this;
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement by starting at
* the last-used {@code majorBegin} and {@code minorBegin} and continues counter-clockwise
* to find a solution. If there was no last-used location set, then RIGHT, BOTTOM is used.
* @return this builder
*/
public PopupWindowPlacerBuilder thenRotateCounterClockwise() {
if (current == null) {
return rotateCounterClockwise(Location.RIGHT, Location.BOTTOM);
}
return rotateCounterClockwise(current.major, current.minorBegin);
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement by starting at
* a point specified by {@code majorBegin} and {@code minorBegin} and continues
* counter-clockwise to find a solution.
* @param majorBegin the major coordinate location of the starting point
* @param minorBegin the minor coordinate location of the starting point
* @return this builder
*/
public PopupWindowPlacerBuilder rotateCounterClockwise(Location majorBegin,
Location minorBegin) {
Location major = majorBegin;
Location minor = minorBegin;
do {
add(new EdgePopupPlacer(major, minor, major.counterClockwise()));
minor = major;
major = major.counterClockwise();
}
while (major != majorBegin);
if (minor != minorBegin) {
// Does remaining portion of initial edge, but repeats first location.
// So if starting at BOTTOM CENTER, will repeat that location in the last partial edge
add(new EdgePopupPlacer(major, minor, minorBegin));
}
return this;
}
/**
* Set the next PopupWindowPlacer to be one that tries to make the placement that is
* allowed to overlap the inner bounds, but with the least overlap area. Tie-breaker
* order is first in this order: Bottom Right, Bottom Left, Top Right, Top Left.
* <p>
* Should never return null, except if using impractical parameters, such as using
* outer bounds that are smaller than inner bounds.
* @return this builder
*/
public PopupWindowPlacerBuilder leastOverlapCorner() {
add(new LeastOverlapCornerPopupWindowPlacer());
return this;
}
/**
* Set the next PopupWindowPlacer that throws an AssertException because no solution has
* been found by the time this placer is tried. This is intended to be used when the coder
* has already guaranteed that there is a solution (i.e., the {@link #leastOverlapCorner()}
* placer has been used and the pop-up area will fit within the outer bounds).
* @return this builder
*/
public PopupWindowPlacerBuilder throwsAssertException() {
add(new ThrowsAssertExceptionPlacer());
return this;
}
}

View File

@@ -0,0 +1,610 @@
/* ###
* 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 docking.widgets.shapes;
import static org.junit.Assert.*;
import java.awt.*;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.util.exception.AssertException;
public class PopupWindowPlacerTest extends AbstractGenericTest {
private Rectangle screen = new Rectangle(0, 0, 2000, 1000);
// This is overly large for testing with a common context
private Rectangle center = new Rectangle(200, 200, 1600, 600);
private Dimension popup = new Dimension(100, 100);
private Dimension popupBig = new Dimension(250, 250);
private Dimension popupHugeWidth = new Dimension(screen.width - 100, 100);
private Dimension popupHugeHeight = new Dimension(100, screen.height - 100);
@Test
public void testLeftmostTop() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.LEFT).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x - popup.width, center.y - popup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testUpperLeft() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.TOP).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x - popup.width, center.y - popup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testLeftmostBottom() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().bottomEdge(Location.LEFT).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x - popup.width, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testLowerLeft() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.BOTTOM).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x - popup.width, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testRightmostTop() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.RIGHT).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x + center.width, center.y - popup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testUpperRight() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().rightEdge(Location.TOP).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x + center.width, center.y - popup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testRightmostBottom() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.RIGHT).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x + center.width, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testLowerRight() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.BOTTOM).build();
Rectangle placement = placer.getPlacement(popup, center, screen);
Point expected = new Point(center.x + center.width, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popup), placement);
}
@Test
public void testLeftmostTopNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.LEFT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.max(center.x - widePopup.width, screen.x);
Point expected = new Point(x, center.y - widePopup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
@Test
public void testLeftmostTopNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.LEFT, Location.LEFT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testUpperLeftNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.TOP).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
int y = Integer.max(center.y - tallPopup.height, screen.y);
Point expected = new Point(center.x - tallPopup.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, tallPopup), placement);
}
@Test
public void testUpperLeftNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().leftEdge(Location.TOP, Location.TOP).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testLeftmostBottomNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().bottomEdge(Location.LEFT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.max(center.x - widePopup.width, screen.x);
Point expected = new Point(x, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
@Test
public void testLeftmostBottomNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.LEFT, Location.LEFT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testLowerLeftNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.BOTTOM).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
int y = Integer.min(center.y + center.height, screen.y + screen.height - tallPopup.height);
Point expected = new Point(center.x - tallPopup.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, tallPopup), placement);
}
@Test
public void testLowerLeftNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().leftEdge(Location.BOTTOM, Location.BOTTOM).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testRightmostTopNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.RIGHT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.min(center.x + center.width, screen.x + screen.width - widePopup.width);
Point expected = new Point(x, center.y - widePopup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
@Test
public void testRightmostTopNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.RIGHT, Location.RIGHT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testUpperRightNeedsShift() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().rightEdge(Location.TOP).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
int y = Integer.max(center.y - tallPopup.height, screen.y);
Point expected = new Point(center.x + center.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, tallPopup), placement);
}
@Test
public void testUpperRightNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.TOP, Location.TOP).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testRightmostBottomNeedsShift() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.RIGHT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.min(center.x + center.width, screen.x + screen.width - widePopup.width);
Point expected = new Point(x, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
@Test
public void testRightmostBottomNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.RIGHT, Location.RIGHT).build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testLowerRightNeedsShift() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.BOTTOM).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
int y = Integer.min(center.y + center.height, screen.y + screen.height - tallPopup.height);
Point expected = new Point(center.x + center.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, tallPopup), placement);
}
@Test
public void testLowerRightNeedsShiftButCannot() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.BOTTOM, Location.BOTTOM).build();
int delta = 200;
Dimension tallPopup = new Dimension(popup.width, popup.height + delta);
Rectangle placement = placer.getPlacement(tallPopup, center, screen);
assertTrue(placement == null);
}
@Test
public void testCenterTopNeedsShiftLeft() {
int deltaX = 100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
int x = Integer.min(skewed.x + (skewed.width - popupHugeWidth.width) / 2,
screen.x + screen.width - popupHugeWidth.width);
Point expected = new Point(x, skewed.y - popupHugeWidth.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeWidth), placement);
}
@Test
public void testCenterTopNeedsShiftLeftButCannot() {
int deltaX = 100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterTopNeedsShiftRight() {
int deltaX = -100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().topEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
int x = Integer.max(skewed.x + (skewed.width - popupHugeWidth.width) / 2, screen.x);
Point expected = new Point(x, skewed.y - popupHugeWidth.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeWidth), placement);
}
@Test
public void testCenterTopNeedsShiftRightButCannot() {
int deltaX = -100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterBottomNeedsShiftLeft() {
int deltaX = 100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
int x = Integer.min(skewed.x + (skewed.width - popupHugeWidth.width) / 2,
screen.x + screen.width - popupHugeWidth.width);
Point expected = new Point(x, skewed.y + skewed.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeWidth), placement);
}
@Test
public void testCenterBottomNeedsShiftLeftDownButCannot() {
int deltaX = 100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterBottomNeedsShiftRight() {
int deltaX = -100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
int x = Integer.max(skewed.x + (skewed.width - popupHugeWidth.width) / 2, screen.x);
Point expected = new Point(x, center.y + center.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeWidth), placement);
}
@Test
public void testCenterBottomNeedsShiftRightDownButCannot() {
int deltaX = -100;
int deltaY = 0;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().bottomEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeWidth, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterLeftNeedsShiftUp() {
int deltaX = 0;
int deltaY = 100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
int y = Integer.min(skewed.y + (skewed.height - popupHugeHeight.height) / 2,
screen.y + screen.height - popupHugeHeight.height);
Point expected = new Point(center.x - popupHugeHeight.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeHeight), placement);
}
@Test
public void testCenterLeftNeedsShiftUpDownButCannot() {
int deltaX = 0;
int deltaY = 100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().leftEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterLeftNeedsShiftDown() {
int deltaX = 0;
int deltaY = -100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leftEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
int y = Integer.max(skewed.y + (skewed.height - popupHugeHeight.height) / 2, screen.y);
Point expected = new Point(center.x - popupHugeHeight.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeHeight), placement);
}
@Test
public void testCenterLeftNeedsShiftDownDownButCannot() {
int deltaX = 0;
int deltaY = -100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().leftEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterRightNeedsShiftUp() {
int deltaX = 0;
int deltaY = 100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
int y = Integer.min(skewed.y + (skewed.height - popupHugeHeight.height) / 2,
screen.y + screen.height - popupHugeHeight.height);
Point expected = new Point(center.x + center.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeHeight), placement);
}
@Test
public void testCenterRightNeedsShiftUpDownButCannot() {
int deltaX = 0;
int deltaY = 100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testCenterRightNeedsShiftDown() {
int deltaX = 0;
int deltaY = -100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
int y = Integer.max(skewed.y + (skewed.height - popupHugeHeight.height) / 2, screen.y);
Point expected = new Point(center.x + center.width, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupHugeHeight), placement);
}
@Test
public void testCenterRightNeedsShiftDownButCannot() {
int deltaX = 0;
int deltaY = -100;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().rightEdge(Location.CENTER, Location.CENTER).build();
Rectangle placement = placer.getPlacement(popupHugeHeight, skewed, screen);
assertTrue(placement == null);
}
@Test
public void testLeastOverlapCornerTopLeft() {
int deltaX = 1;
int deltaY = 1;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leastOverlapCorner().build();
Rectangle placement = placer.getPlacement(popupBig, skewed, screen);
int x = Integer.max(skewed.x - popupBig.width, screen.x);
int y = Integer.max(skewed.y - popupBig.height, screen.y);
Point expected = new Point(x, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupBig), placement);
}
// Overlapping corner tests
@Test
public void testLeastOverlapCornerBottomLeft() {
int deltaX = 1;
int deltaY = -1;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leastOverlapCorner().build();
Rectangle placement = placer.getPlacement(popupBig, skewed, screen);
int x = Integer.max(skewed.x - popupBig.width, screen.x);
int y = Integer.min(skewed.y + skewed.height, screen.y + screen.height - popupBig.height);
Point expected = new Point(x, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupBig), placement);
}
@Test
public void testLeastOverlapCornerTopRight() {
int deltaX = -1;
int deltaY = 1;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leastOverlapCorner().build();
Rectangle placement = placer.getPlacement(popupBig, skewed, screen);
int x = Integer.min(skewed.x + skewed.width, screen.x + screen.width - popupBig.width);
int y = Integer.max(skewed.y - popupBig.height, screen.y);
Point expected = new Point(x, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupBig), placement);
}
@Test
public void testLeastOverlapCornerBottomRight() {
int deltaX = -1;
int deltaY = -1;
Rectangle skewed =
new Rectangle(center.x + deltaX, center.y + deltaY, center.width, center.height);
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().leastOverlapCorner().build();
Rectangle placement = placer.getPlacement(popupBig, skewed, screen);
int x = Integer.min(skewed.x + skewed.width, screen.x + screen.width - popupBig.width);
int y = Integer.min(skewed.y + skewed.height, screen.y + screen.height - popupBig.height);
Point expected = new Point(x, y);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, popupBig), placement);
}
@Test
public void testThrowsAssertException() {
PopupWindowPlacer placer = new PopupWindowPlacerBuilder().throwsAssertException().build();
try {
// Choice of context area and other parameters does not matter for this test
placer.getPlacement(popup, center, screen);
fail("Should not get here");
}
catch (AssertException e) {
assertTrue("Unexpected popup placement error.".equals(e.getMessage()));
}
}
// Some Combination tests
@Test
public void testLeftmostTopFixedRightmostTopShift() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.LEFT, Location.LEFT)
.topEdge(Location.RIGHT)
.build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.min(center.x + center.width, screen.x + screen.width - widePopup.width);
Point expected = new Point(x, center.y - widePopup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
@Test
public void testRightmostTopFixedLeftmostTopShift() {
PopupWindowPlacer placer =
new PopupWindowPlacerBuilder().topEdge(Location.RIGHT, Location.RIGHT)
.topEdge(Location.LEFT)
.build();
int delta = 200;
Dimension widePopup = new Dimension(popup.width + delta, popup.height);
Rectangle placement = placer.getPlacement(widePopup, center, screen);
int x = Integer.max(center.x - widePopup.width, screen.x);
Point expected = new Point(x, center.y - widePopup.height);
assertTrue(screen.contains(placement));
assertEquals(new Rectangle(expected, widePopup), placement);
}
}