HoverService provides the ability to popup data Windows over a Field viewer
@@ -30,7 +29,8 @@ import docking.widgets.fieldpanel.support.FieldLocation;
public interface HoverService {
/**
- * Returns the priority of this hover service.
+ * Returns the priority of this hover service. A lower priority is more important.
+ * @return the priority
*/
public int getPriority();
@@ -41,7 +41,8 @@ public interface HoverService {
public void scroll(int amount);
/**
- * Return whether hover mode is "on."
+ * Return whether hover mode is "on"
+ * @return the priority
*/
public boolean hoverModeSelected();
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java
index 7af4d68ce8..e29c102bc2 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java
@@ -17,6 +17,7 @@ package docking.widgets;
import java.awt.*;
import java.awt.event.*;
+import java.awt.geom.Rectangle2D;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
@@ -25,7 +26,9 @@ import java.util.List;
import javax.swing.*;
import javax.swing.Timer;
+import generic.json.Json;
import generic.util.WindowUtilities;
+import ghidra.util.Msg;
import ghidra.util.bean.GGlassPane;
import ghidra.util.bean.GGlassPanePainter;
@@ -112,8 +115,8 @@ public class PopupWindow {
sourceMouseMotionListener = new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
- Point localPoint = e.getPoint();
+ Point localPoint = e.getPoint();
SwingUtilities.convertPointToScreen(localPoint, e.getComponent());
if (!neutralMotionZone.contains(localPoint)) {
hide();
@@ -121,7 +124,7 @@ public class PopupWindow {
else {
// If the user mouses around the neutral zone, then start the close timer. The
// timer will be reset if the user enters the popup.
- closeTimer.start();
+ closeTimer.restart();
}
e.consume(); // consume the event so that the source component doesn't processes it
}
@@ -237,16 +240,15 @@ public class PopupWindow {
int y = point.y + keepVisibleArea.height + Y_PADDING;
popupBounds.setLocation(x, y);
- WindowUtilities.ensureOnScreen(sourceComponent, popupBounds);
+ ensureSize(popupBounds);
Rectangle hoverArea = new Rectangle(point, keepVisibleArea);
- adjustBoundsForCursorLocation(popupBounds, hoverArea);
-
- neutralMotionZone = createNeutralMotionZone(popupBounds, hoverArea);
+ Rectangle adjustedBounds = adjustBoundsForCursorLocation(popupBounds, hoverArea);
+ neutralMotionZone = createNeutralMotionZone(adjustedBounds, hoverArea);
installDebugPainter(e);
- popup.setBounds(popupBounds);
+ popup.setBounds(adjustedBounds);
popup.setVisible(true);
removeOldPopupReferences();
@@ -254,19 +256,38 @@ public class PopupWindow {
VISIBLE_POPUPS.add(new WeakReference<>(this));
}
+ private void ensureSize(Rectangle popupBounds) {
+ Shape screenShape = WindowUtilities.getVisibleScreenBounds();
+ Rectangle screenBounds = screenShape.getBounds();
+ if (screenBounds.width < popupBounds.width) {
+ popupBounds.width = screenBounds.width / 2;
+ }
+
+ if (screenBounds.height < popupBounds.height) {
+ popupBounds.height = screenBounds.height / 2;
+ }
+ }
+
+ private void installDebugPainter(List
- * When the hoverArea is obscured, this method will first attempt to move the
- * bounds up if possible. If moving up is not possible due to space constraints, then this
- * method will try to shift the bounds to the right of the hover area. If this is not
- * possible, then the bounds will not be changed.
+ * When the hoverArea is obscured, this method will create a grid of possible locations
+ * in which to place the given bounds. The grid will be searched for the location that is
+ * closest to the hover area without touching it.
*
* @param bounds The bounds to move as necessary.
* @param hoverArea The area that should not be covered by the given bounds
@@ -274,28 +295,113 @@ public class PopupWindow {
* if possible.
*/
private Rectangle adjustBoundsForCursorLocation(Rectangle bounds, Rectangle hoverArea) {
- if (!bounds.intersects(hoverArea)) {
+ Shape screenShape = WindowUtilities.getVisibleScreenBounds();
+ Rectangle screenBounds = screenShape.getBounds();
+ if (!bounds.intersects(hoverArea) && screenBounds.contains(bounds)) {
return bounds;
}
- // first attempt to move the window--try to go up
- int movedY = hoverArea.y - bounds.height;
- boolean canMoveUp = movedY >= 0;
- if (canMoveUp) {
- // move the given bounds above the current point
- bounds.y = movedY;
+ // center bounds over hover area; we intend not to block the hover area
+ int dx = (hoverArea.width / 2) - (bounds.width / 2);
+ int dy = (hoverArea.height / 2) - (bounds.height / 2);
+ Point hoverCenter = bounds.getLocation();
+ hoverCenter.x += dx;
+ hoverCenter.y += dy;
+
+ List
- * When the hoverArea is obscured, this method will create a grid of possible locations
- * in which to place the given bounds. The grid will be searched for the location that is
- * closest to the hover area without touching it.
- *
- * @param bounds The bounds to move as necessary.
- * @param hoverArea The area that should not be covered by the given bounds
- * @return the original bounds adjusted so that they do not cover the given hoverArea,
- * if possible.
- */
- private Rectangle adjustBoundsForCursorLocation(Rectangle bounds, Rectangle hoverArea) {
- Shape screenShape = WindowUtilities.getVisibleScreenBounds();
- Rectangle screenBounds = screenShape.getBounds();
- if (!bounds.intersects(hoverArea) && screenBounds.contains(bounds)) {
- return bounds;
- }
-
- // center bounds over hover area; we intend not to block the hover area
- int dx = (hoverArea.width / 2) - (bounds.width / 2);
- int dy = (hoverArea.height / 2) - (bounds.height / 2);
- Point hoverCenter = bounds.getLocation();
- hoverCenter.x += dx;
- hoverCenter.y += dy;
-
- List
+ * 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.
+ *
+ *
+ * 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.
+ */
+public abstract class PopupWindowPlacer {
+
+ protected Location major;
+ protected Location minorBegin;
+ protected Location minorEnd;
+
+ private PopupWindowPlacer next = null;
+
+ /**
+ * Constructor only for classes that do you use Preferences
+ */
+ public PopupWindowPlacer() {
+ // Only for implementations that do not use 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
+ */
+ 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 exceeded 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):
+ *
+ * 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
+ * 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,
+ *
+ * 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.,
+ * 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,
+ *
+ * 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:
+ *
+ * ROTATIONAL PLACERS
+ *
+ * There are clockwise and counter-clockwise rotational placers that built up from edge placers.
+ * These are:
+ *
+ * OVERLAPPING CORNER PLACER
+ *
+ * There is one corner placer,
+ * ASSERT PLACER
+ *
+ * The
+ * COMPOSITE EXAMPLES
+ * 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:
+ *
+ * Builds a placer that first attempts each of the four major corners in a specific order, with no
+ * shifting, followed by an assertion failure:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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/PopupWindowPlacerTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/PopupWindowPlacerTest.java
new file mode 100644
index 0000000000..7e2ad360f9
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/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;
+
+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);
+ }
+}
From f978164cfc4c05ab35408ce2d1eb260a4a93887b Mon Sep 17 00:00:00 2001
From: dragonmacher <48328597+dragonmacher@users.noreply.github.com>
Date: Thu, 3 Jun 2021 11:18:24 -0400
Subject: [PATCH 3/3] GP-875 - Popup window placement integration
---
.../core/hover/AbstractHoverProvider.java | 17 +++++-
.../java/docking/widgets/PopupWindow.java | 11 ++--
.../widgets/{ => shapes}/Location.java | 2 +-
.../{ => shapes}/PopupWindowPlacer.java | 18 +++---
.../PopupWindowPlacerBuilder.java | 55 +++++++++++++++----
.../{ => shapes}/PopupWindowPlacerTest.java | 2 +-
6 files changed, 78 insertions(+), 27 deletions(-)
rename Ghidra/Framework/Docking/src/main/java/docking/widgets/{ => shapes}/Location.java (98%)
rename Ghidra/Framework/Docking/src/main/java/docking/widgets/{ => shapes}/PopupWindowPlacer.java (98%)
rename Ghidra/Framework/Docking/src/main/java/docking/widgets/{ => shapes}/PopupWindowPlacerBuilder.java (96%)
rename Ghidra/Framework/Docking/src/test/java/docking/widgets/{ => shapes}/PopupWindowPlacerTest.java (99%)
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/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java
index ec2ffda275..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,10 +25,16 @@ 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 = 25;
private static final int Y_PADDING = 25;
@@ -302,15 +308,12 @@ public class PopupWindow {
// painters.add(painter);
}
- // for debug
-// private static List
- * Algorithm Design...
+ * 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
@@ -283,7 +287,7 @@ public abstract class PopupWindowPlacer {
* 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 exceeded the outer bounds (screen) rectangle. But,
+ * 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
@@ -348,7 +352,7 @@ public abstract class PopupWindowPlacer {
* else
* no solution
*
- * ... or...
+ * or
*
- * 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
@@ -38,7 +42,10 @@ import docking.widgets.PopupWindowPlacer.*;
* same way for each method, though some arguments are not valid for some edges; for instance,
*
- * 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
@@ -46,7 +53,10 @@ import docking.widgets.PopupWindowPlacer.*;
* far the solution is allowed to shift. To specify a "no-shift" solution, one specifies the same
* placement cell twice (e.g.,
- * 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,
*
- * 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:
*
- * ROTATIONAL PLACERS
+ *
* There are clockwise and counter-clockwise rotational placers that built up from edge placers.
* These are:
@@ -96,7 +116,11 @@ import docking.widgets.PopupWindowPlacer.*;
* 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,
- * ASSERT PLACER
+ *
* The
- * COMPOSITE EXAMPLES
+ *
* 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:
diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/PopupWindowPlacerTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java
similarity index 99%
rename from Ghidra/Framework/Docking/src/test/java/docking/widgets/PopupWindowPlacerTest.java
rename to Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java
index 7e2ad360f9..a94430a2d4 100644
--- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/PopupWindowPlacerTest.java
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/shapes/PopupWindowPlacerTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package docking.widgets;
+package docking.widgets.shapes;
import static org.junit.Assert.*;
+ *
+ * +-----------------------------------------------+
+ * | 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
+ * | | | |
+ * +---------------+---------------+---------------+
+ *
+ *
+ *
+ * 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/PopupWindowPlacerBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindowPlacerBuilder.java
new file mode 100644
index 0000000000..e6e4bf0814
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindowPlacerBuilder.java
@@ -0,0 +1,396 @@
+/* ###
+ * 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;
+
+import docking.widgets.PopupWindowPlacer.*;
+
+/**
+ * This class builds a PopWindowPlacer that can have subsequent PopWindowPlacers.
+ * 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.
+ * Location.TOP is only valid for left and right edges.
+ * rightEdge(Location.TOP, Location.TOP)).
+ * 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.
+ *
+ * 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();
+ *
+ * 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.
+ * 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.
+ * 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.
+ *
+ * PopupWindowPlacer placer =
+ * new PopupWindowPlacerBuilder()
+ * .rightEdge()
+ * .leftEdge()
+ * .topEdge()
+ * .bottomEdge()
+ * .leastOverlapCorner()
+ * .throwsAssertException()
+ * .build();
+ *
+ * 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();
+ *
+ * PopupWindowPlacer placer =
+ * new PopupWindowPlacerBuilder()
+ * .rightEdge(Location.BOTTOM)
+ * .bottomEdge(Location.RIGHT)
+ * .build();
+ *
+ * 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.
+ *
* lesserLocation
* → lesserBoundedLocation (≥ lesserLocation)
@@ -369,7 +373,7 @@ public abstract class PopupWindowPlacer {
* else
* solution is centerLocation
*
- * ... or...
+ * or
*
* lesserLocation
* centerLocation
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindowPlacerBuilder.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java
similarity index 96%
rename from Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindowPlacerBuilder.java
rename to Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java
index e6e4bf0814..f49f3e4c22 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindowPlacerBuilder.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/shapes/PopupWindowPlacerBuilder.java
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package docking.widgets;
+package docking.widgets.shapes;
-import docking.widgets.PopupWindowPlacer.*;
+import docking.widgets.shapes.PopupWindowPlacer.*;
/**
* This class builds a PopWindowPlacer that can have subsequent PopWindowPlacers.
@@ -24,7 +24,11 @@ import docking.widgets.PopupWindowPlacer.*;
* placers, and a clean-up assert placer. Additionally, there are rotational placers
* that are composed of edge placers.
*
+ *
+ *
+ * Edge Placers
+ *
* Location.TOP is only valid for left and right edges.
* Two or More Location Arguments
+ *
+ * rightEdge(Location.TOP, Location.TOP)).
* One Location Argument
+ *
+ * rightEdge(Location.TOP) is the same as
@@ -54,11 +64,17 @@ import docking.widgets.PopupWindowPlacer.*;
* 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
+ *
+ * Examples
+ *
+ *
@@ -77,7 +93,11 @@ import docking.widgets.PopupWindowPlacer.*;
* .topEdge(Location.CENTER);
* .build();
*
+ *
+ *
+ * Rotational Placers
+ *
*
+ *
+ *
+ * Overlapping 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
@@ -106,7 +130,11 @@ import docking.widgets.PopupWindowPlacer.*;
* top left. Unless ill-constructed (sized of context area, screen, and pop-up dimension), this
* placer should always find a solution.
*
+ *
+ *
+ * Assert Placer
+ *
* 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
@@ -114,7 +142,12 @@ import docking.widgets.PopupWindowPlacer.*;
* leastOverlapCorner() placer. This just throws an exception instead of returning
* the null return value that would be returned from previous placement attempts.
*
+ *
+ *
+ * Composite Placer
+ *
+ *