diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java new file mode 100644 index 0000000000..a247e899b2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHover.java @@ -0,0 +1,92 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser.hover; + +import javax.swing.JComponent; +import javax.swing.JToolTip; + +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.GhidraOptions; +import ghidra.app.plugin.core.hover.AbstractConfigurableHover; +import ghidra.app.util.ToolTipUtils; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.*; + +/** + * A Listing hover to show tool tips for function signatures + */ +public class FunctionSignatureListingHover extends AbstractConfigurableHover + implements ListingHoverService { + + private static final String NAME = "Function Signature Display"; + private static final String DESCRIPTION = + "Toggle whether function signature is displayed in a tooltip " + + "when the mouse hovers over a function signature."; + + // note: guilty knowledge that the Truncated Text service has a priority of 10 + private static final int POPUP_PRIORITY = 20; + + public FunctionSignatureListingHover(PluginTool tool) { + super(tool, POPUP_PRIORITY); + } + + @Override + protected String getName() { + return NAME; + } + + @Override + protected String getDescription() { + return DESCRIPTION; + } + + @Override + protected String getOptionsCategory() { + return GhidraOptions.CATEGORY_BROWSER_POPUPS; + } + + @Override + public JComponent getHoverComponent(Program program, ProgramLocation programLocation, + FieldLocation fieldLocation, Field field) { + + if (!enabled || programLocation == null) { + return null; + } + + Class clazz = programLocation.getClass(); + if (clazz != FunctionSignatureFieldLocation.class && + clazz != FunctionNameFieldLocation.class) { + return null; + } + + // is the label local to the function + FunctionSignatureFieldLocation functionLocation = + (FunctionSignatureFieldLocation) programLocation; + + Address entry = functionLocation.getFunctionAddress(); + FunctionManager functionManager = program.getFunctionManager(); + Function function = functionManager.getFunctionAt(entry); + + String toolTipText = ToolTipUtils.getToolTipText(function, true); + JToolTip toolTip = new JToolTip(); + toolTip.setTipText(toolTipText); + return toolTip; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java new file mode 100644 index 0000000000..301c30bf5d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/FunctionSignatureListingHoverPlugin.java @@ -0,0 +1,51 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.codebrowser.hover; + +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * A plugin to show tool tip text for a function signature + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.CODE_VIEWER, + shortDescription = "Shows formatted tool tip text over function signatures", + description = "This plugin extends the functionality of the code browser by adding a " + + "\tooltip\" over function signaturefields in Listing.", + servicesProvided = { ListingHoverService.class } +) +//@formatter:on +public class FunctionSignatureListingHoverPlugin extends Plugin { + + private FunctionSignatureListingHover functionSignatureHover; + + public FunctionSignatureListingHoverPlugin(PluginTool tool) { + super(tool); + functionSignatureHover = new FunctionSignatureListingHover(tool); + registerServiceProvided(ListingHoverService.class, functionSignatureHover); + } + + @Override + protected void dispose() { + functionSignatureHover.dispose(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java index 2a4a22ac0d..78139cbc5d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/hover/ProgramAddressRelationshipListingHover.java @@ -123,9 +123,9 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable return; } - String dataDescr = "Data Offset"; + String description = "Data Offset"; if (data.getDataType() instanceof Structure) { - dataDescr = "Structure Offset"; + description = "Structure Offset"; } String name = data.getLabel(); // prefer the label @@ -133,12 +133,14 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable name = data.getDataType().getName(); } + name = StringUtilities.trimMiddle(name, 60); + if (name == null) { // don't think we can get here name = italic("Unnamed"); } - appendTableRow(sb, dataDescr, name, dataOffset); + appendTableRow(sb, description, name, dataOffset); } private void addByteSourceInfo(Program program, Address loc, StringBuilder sb) { @@ -150,7 +152,8 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable if (addressSourceInfo.getFileName() == null) { return; } - String filename = StringUtilities.trim(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE); + String filename = + StringUtilities.trimMiddle(addressSourceInfo.getFileName(), MAX_FILENAME_SIZE); long fileOffset = addressSourceInfo.getFileOffset(); String dataDescr = "Byte Source Offset"; appendTableRow(sb, dataDescr, "File: " + filename, fileOffset); @@ -160,7 +163,10 @@ public class ProgramAddressRelationshipListingHover extends AbstractConfigurable Function function = program.getFunctionManager().getFunctionContaining(loc); if (function != null) { long functionOffset = loc.subtract(function.getEntryPoint()); - appendTableRow(sb, "Function Offset", HTMLUtilities.escapeHTML(function.getName()), + + String functionName = function.getName(); + functionName = StringUtilities.trimMiddle(functionName, 60); + appendTableRow(sb, "Function Offset", HTMLUtilities.escapeHTML(functionName), functionOffset); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractHoverProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractHoverProvider.java index 3eccd3e4bf..ef7562bad0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractHoverProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractHoverProvider.java @@ -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); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java index 54f9c71394..0da7e2667a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/hover/AbstractReferenceHover.java @@ -91,14 +91,15 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover { String hoverName = getName(); options.getOptions(hoverName).setOptionsHelpLocation(help); + options.registerOption(hoverName, true, null, getDescription()); + enabled = options.getBoolean(hoverName, true); options.registerOption(hoverName + Options.DELIMITER + "Dialog Height", 400, help, "Height of the popup window"); options.registerOption(hoverName + Options.DELIMITER + "Dialog Width", 600, help, "Width of the popup window"); - setOptions(options, hoverName); options.addOptionsChangeListener(this); } @@ -152,10 +153,18 @@ public abstract class AbstractReferenceHover extends AbstractConfigurableHover { return; } + toolTip = new JToolTip(); + panel = new ListingPanel(codeFormatService.getFormatManager());// share the manager from the code viewer panel.setTextBackgroundColor(BACKGROUND_COLOR); - toolTip = new JToolTip(); + String name = getName(); + String widthOptionName = name + Options.DELIMITER + "Dialog Width"; + String heightOptionName = name + Options.DELIMITER + "Dialog Height"; + int dialogWidth = options.getInt(widthOptionName, 600); + int dialogHeight = options.getInt(heightOptionName, 400); + Dimension d = new Dimension(dialogWidth, dialogHeight); + panel.setPreferredSize(d); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java index 434a2cbb1c..c303b0b6b6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/HoverService.java @@ -15,13 +15,12 @@ */ package ghidra.app.services; -import ghidra.program.model.listing.Program; -import ghidra.program.util.ProgramLocation; - import javax.swing.JComponent; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; /** * HoverService provides the ability to popup data Windows over a Field viewer @@ -30,7 +29,8 @@ import docking.widgets.fieldpanel.support.FieldLocation; public interface HoverService { /** - * Returns the priority of this hover service. + * Returns the priority of this hover service. A lower priority is more important. + * @return the priority */ public int getPriority(); @@ -41,7 +41,8 @@ public interface HoverService { public void scroll(int amount); /** - * Return whether hover mode is "on." + * Return whether hover mode is "on" + * @return the priority */ public boolean hoverModeSelected(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java index 7af4d68ce8..48a911fe3d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java @@ -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> 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> iterator = - VISIBLE_POPUPS.iterator(); iterator.hasNext();) { + for (Iterator> iterator = VISIBLE_POPUPS.iterator(); iterator + .hasNext();) { WeakReference 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 after 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. - *

- * When the hoverArea is obscured, this method will first attempt to move the - * bounds up if possible. If moving up is not possible due to space constraints, then this - * method will try to shift the bounds to the right of the hover area. If this is not - * possible, then the bounds will not be changed. - * - * @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 hoverArea, - * 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 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)); + } } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/Location.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/Location.java new file mode 100644 index 0000000000..b72558b8a6 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/Location.java @@ -0,0 +1,92 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacer.java new file mode 100644 index 0000000000..0ee33e1d34 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacer.java @@ -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. + *

+ * 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. + *

+ *
+ *        +-----------------------------------------------+
+ *        |                                        outer  |
+ *        |                                               |
+ *        |                                               |
+ *        |      +------------------+                     |
+ *        |      |       good       |                     |
+ *        |      |     placement    |                     |
+ *        |      |                  |                     |
+ *        |      +------------------+---------+           |
+ *        |                         |         |           |
+ *        |                         |  inner  |           |
+ *        |                         |         |           |
+ *        |                         +---------+------------------+
+ *        |                                   |       bad |      |
+ *        |                                   |     placement    |
+ *        +-----------------------------------+-----------+      |
+ *                                            +------------------+
+ *
+ * 
+ * + * 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. + *
+ *
+ *              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
+ *        |               |               |               |
+ *        +---------------+---------------+---------------+
+ *
+ * 
+ *

+ * + * 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. + *

+ * If the chosen cell is a CENTER cell, then it could shift up or down, depending on the + * circumstances and the parameters applied. + *

+ * These placements and shifts are controlled by specifying the major and minorBegin + * and minorEnd {@link Location Locations}. The major Location specifies the edge + * for an {@link EdgePopupPlacer} and the minorBegin Location specifies the placement cell + * 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. + *

+ * Combinations of these placement attempts can be put together to create more complex strategies. + * See {@link PopupWindowPlacerBuilder} for examples of these. + *

+ * 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. + *

+ * When dealing with solutions for the top or bottom edge, we are considering vertical + * to be the major axis with y/height values representing that axis, and horizontal + * 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. + *

+ * Algorithm Design

+ * Note: smaller values are up and bigger values are down, in the presentation below. + *

+ * 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. + *

+ * 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. + *

+ * The scenario with top and bottom edge reverses the major dimension to be vertical and the + * minor dimension to be horizontal. + *

+ * 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. + *

+ * 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 positive lengths): + *

+	 *         lesserLocation (nominal placement on TOP or LEFT)
+	 *               ≤
+	 *         centerLocation (nominal placement such that the center of the context rectangle
+	 *                          and the center of the popup area align with each other)
+	 *               ≤
+	 *         greaterLocation (nominal placement on BOTTOM or RIGHT)
+	 * 
+ * and these two can be found at various placements amongst the other three: + *
+	 *         lesserBoundedLocation ≥ lesserLocation (lesserLocation shifted so TOP or
+	 *             LEFT fits outer bounds)
+	 *         greaterBoundedLocation ≤ greaterLocation (lesserLocation shifted BOTTOM or
+	 *             RIGHT fits outer bounds)
+	 * 
+ * Note that with an ill-constructed scenario, as shown here, we return no solution: + *
+	 *         greaterBoundedLocation
+	 *                   <
+	 *         lesserBoundedLocation
+	 * 
+ * Given a better-constructed scenario, the lesserBoundedLocation and + * greaterBoundedLocation values can fall between the other three values at the following + * possible locations: + *
+	 *         lesserLocation
+	 *             → lesserBoundedLocation (≥ lesserLocation)
+	 *             → greaterBoundedLocation (≤ greaterLocation)
+	 *         centerLocation
+	 *             → lesserBoundedLocation (≥ lesserLocation)
+	 *             → greaterBoundedLocation (≤ greaterLocation)
+	 *         greaterLocation
+	 * 
+ * These layout possibilities can be broken down into three possibilities... + *
+	 *         lesserLocation
+	 *             → lesserBoundedLocation (≥ lesserLocation)
+	 *             → greaterBoundedLocation
+	 *         centerLocation
+	 *         greaterLocation
+	 *         ----------
+	 *         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
+	 * 
+ * or + *
+	 *         lesserLocation
+	 *             → lesserBoundedLocation (≥ lesserLocation)
+	 *         centerLocation
+	 *             → greaterBoundedLocation (≤ greaterLocation)
+	 *         greaterLocation
+	 *         ----------
+	 *         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
+	 * 
+ * or + *
+	 *         lesserLocation
+	 *         centerLocation
+	 *             → lesserBoundedLocation
+	 *             → greaterBoundedLocation (≤ greaterLocation)
+	 *         greaterLocation
+	 *         ----------
+	 *         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
+	 * 
+ * 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 major edge of the inner bounds, with + * minorBegin specifying the preferred cell location at which to start the + * placement attempt and minorEnd 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; + } + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java new file mode 100644 index 0000000000..f49f3e4c22 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java @@ -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. + *

+ * General categories of placers available are edge placers, overlapped-corner + * placers, and a clean-up assert placer. Additionally, there are rotational placers + * that are composed of edge placers. + *

+ *
+ *
+ * + *

Edge Placers

+ * + *

+ * The edge 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 rightEdge(Location.TOP). 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. + *

+ * 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, + * Location.TOP is only valid for left and right edges. + *

+ * + *

Two or More Location Arguments

+ * + *

+ * 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., rightEdge(Location.TOP, Location.TOP)). + *

+ * + *

One Location Argument

+ * + *

+ * 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, + * rightEdge(Location.TOP) is the same as + * rightEdge(Location.TOP, Location.CENTER). When the single argument is + * Location.CENTER, two attempts are built, the first being the BOTTOM or RIGHT cell + * and the second being the TOP or LEFT cell. + *

+ * + *

No Arguments

+ * + *

+ * 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. + *

+ * + *

Examples

+ * + *

+ * 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: + *

+ *    PopupWindowPlacer placer =
+ *        new PopupWindowPlacerBuilder()
+ *            .rightEdge(Location.BOTTOM,Location.BOTTOM)
+ *            .rightEdge(Location.TOP, Location.TOP)
+ *            .topEdge(Location.CENTER, Location.CENTER)
+ *            .build();
+ * 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: + *
+ *    PopupWindowPlacer placer =
+ *        new PopupWindowPlacerBuilder()
+ *            .rightEdge()
+ *            .topEdge(Location.CENTER);
+ *            .build();
+ *

+ *
+ *
+ * + *

Rotational Placers

+ * + *

+ * There are clockwise and counter-clockwise rotational placers that built up from edge placers. + * These are: + *

+ *    rotateClockwise(Location major, Location minor)
+ *    rotateCounterClockwise(Location major, Location minor)
+ *    thenRotateClockwise()
+ *    thenRotateCounterClockwise()
+ * The first two of these take two Location arguments the specify the starting cell. For instance, + * rotateClockwise(Location.BOTTOM, Location.RIGHT). 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 rotateCounterClockwise placer works the + * same, but in a counter-clockwise fashion. The thenRotateClockwise and + * thenRotateCounterClockwise 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. + *

+ *
+ *
+ * + *

Overlapping Corner Placer

+ * + *

+ * There is one corner placer, leastOverlapCorner(). 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. + *

+ *
+ *
+ * + *

Assert Placer

+ * + *

+ * The throwsAssertException() 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 + * leastOverlapCorner() placer. This just throws an exception instead of returning + * the null return value that would be returned from previous placement attempts. + *

+ *
+ *
+ * + *

Composite Placer

+ * + *

+ * 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: + *

+ *    PopupWindowPlacer placer =
+ *        new PopupWindowPlacerBuilder()
+ *            .rightEdge()
+ *            .leftEdge()
+ *            .topEdge()
+ *            .bottomEdge()
+ *            .leastOverlapCorner()
+ *            .throwsAssertException()
+ *            .build();
+ *

+ * Builds a placer that first attempts each of the four major corners in a specific order, with no + * shifting, followed by an assertion failure: + *

+ *    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();
+ *

+ * 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: + *

+ *    PopupWindowPlacer placer =
+ *        new PopupWindowPlacerBuilder()
+ *            .rightEdge(Location.BOTTOM)
+ *            .bottomEdge(Location.RIGHT)
+ *            .build();
+ *

+ * 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: + *

+ *    PopupWindowPlacer placer =
+ *        new PopupWindowPlacerBuilder()
+ *            .topEdge(Location.LEFT, Location.LEFT)
+ *            .thenRotateClockwise()
+ *            .throwsAssertException()
+ *            .build();
+ * + * @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. + *

+ * 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; + } + +} diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java new file mode 100644 index 0000000000..a94430a2d4 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java @@ -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); + } +}