From a3867840d87945ac910c24d24f77646128b37aed Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:52:51 +0100 Subject: [PATCH 01/12] Implementation of positioning anchor for Stage --- .../src/main/java/javafx/stage/Stage.java | 229 +++++++++++++++++- .../src/main/java/javafx/stage/Window.java | 5 + .../java/test/javafx/stage/StageTest.java | 205 +++++++++++++++- 3 files changed, 431 insertions(+), 8 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index f3016ebd5ea..3e8b48895b4 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -37,6 +38,7 @@ import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.geometry.NodeOrientation; +import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.input.KeyCombination; @@ -52,6 +54,7 @@ import com.sun.javafx.stage.StagePeerListener; import com.sun.javafx.tk.TKStage; import com.sun.javafx.tk.Toolkit; +import com.sun.javafx.util.Utils; import javafx.beans.NamedArg; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; @@ -297,6 +300,33 @@ public Stage(@NamedArg(value="style", defaultValue="DECORATED") StageStyle style super.show(); } + /** + * Shows this stage at the specified location and adjusts the position as needed to keep the stage + * visible on screen. If the stage is already showing, it is moved to the computed position instead. + *

+ * Positioning is done using an {@code anchor} point on the stage. The specified location is interpreted + * as the desired screen coordinates of that anchor, and the stage location is derived from that. + * For example, if the anchor is {@code (0.5, 0.5)}, the stage is positioned so its center lies at + * {@code (anchorX, anchorY)}; if the anchor is {@code (0, 0)}, the stage's top-left corner is placed + * at {@code (anchorX, anchorY)}. The final stage location is clamped to the screen bounds. + *

+ * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will + * reflect the adjusted position. + * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @since 26 + */ + public final void show(double anchorX, double anchorY, Anchor anchor) { + if (isShowing()) { + new PositionRequest(anchorX, anchorY, anchor).apply(this); + } else { + positionRequest = new PositionRequest(anchorX, anchorY, anchor); + super.show(); + } + } + private boolean primary = false; //------------------------------------------------------------------ @@ -431,7 +461,46 @@ private boolean isImportant() { * @since JavaFX 2.2 */ public void showAndWait() { + verifyCanShowAndWait(); + super.show(); + inNestedEventLoop = true; + Toolkit.getToolkit().enterNestedEventLoop(this); + } + + /** + * Shows this stage at the specified location and adjusts the position as needed to keep the stage + * visible on screen. This method blocks until the stage is hidden before returning to the caller. + * This method temporarily blocks processing of the current event and starts a nested event loop + * to handle other events. This method must be called on the JavaFX application thread. + *

+ * Positioning is done using an {@code anchor} point on the stage. The specified location is interpreted + * as the desired screen coordinates of that anchor, and the stage location is derived from that. + * For example, if the anchor is {@code (0.5, 0.5)}, the stage is positioned so its center lies at + * {@code (anchorX, anchorY)}; if the anchor is {@code (0, 0)}, the stage's top-left corner is placed + * at {@code (anchorX, anchorY)}. The final stage location is clamped to the screen bounds. + *

+ * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will + * reflect the adjusted position. + * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread + * @throws IllegalStateException if this method is called during animation or layout processing + * @throws IllegalStateException if this call would exceed the maximum number of nested event loops + * @throws IllegalStateException if this method is called on the primary stage + * @throws IllegalStateException if this stage is already showing + * @since 26 + */ + public final void showAndWait(double anchorX, double anchorY, Anchor anchor) { + verifyCanShowAndWait(); + positionRequest = new PositionRequest(anchorX, anchorY, anchor); + super.show(); + inNestedEventLoop = true; + Toolkit.getToolkit().enterNestedEventLoop(this); + } + private void verifyCanShowAndWait() { Toolkit.getToolkit().checkFxUserThread(); if (isPrimary()) { @@ -450,10 +519,6 @@ public void showAndWait() { // method is called from an event handler that is listening to a // WindowEvent.WINDOW_HIDING event. assert !inNestedEventLoop; - - show(); - inNestedEventLoop = true; - Toolkit.getToolkit().enterNestedEventLoop(this); } private StageStyle style; // default is set in constructor @@ -1315,4 +1380,160 @@ private void setPrefHeaderButtonHeight(double height) { peer.setPrefHeaderButtonHeight(height); } } + + @Override + final void fixBounds() { + if (positionRequest != null) { + positionRequest.apply(this); + positionRequest = null; + } + } + + private PositionRequest positionRequest; + + private record PositionRequest(double screenX, double screenY, Anchor anchor) { + + void apply(Stage stage) { + Screen currentScreen = Utils.getScreenForPoint(screenX, screenY); + Rectangle2D screenBounds = Utils.hasFullScreenStage(currentScreen) + ? currentScreen.getBounds() + : currentScreen.getVisualBounds(); + + double width = stage.getWidth(); + double height = stage.getHeight(); + double anchorX, anchorY; + double anchorRelX, anchorRelY; + + if (anchor.relative) { + anchorX = width * anchor.x; + anchorY = height * anchor.y; + anchorRelX = anchor.x; + anchorRelY = anchor.y; + } else { + anchorX = anchor.x; + anchorY = anchor.y; + anchorRelX = anchor.x / width; + anchorRelY = anchor.y / height; + } + + double minX = screenBounds.getMinX(); + double minY = screenBounds.getMinY(); + double maxX = screenBounds.getMaxX() - width; + double maxY = screenBounds.getMaxY() - height; + double x, y; + + if (maxX >= minX) { + x = Utils.clamp(minX, screenX - anchorX, maxX); + } else { + x = anchorRelX > 0.5 ? maxX : minX; + } + + if (maxY >= minY) { + y = Utils.clamp(minY, screenY - anchorY, maxY); + } else { + y = anchorRelY > 0.5 ? maxY : minY; + } + + stage.setX(x); + stage.setY(y); + } + } + + /** + * Defines an anchor point that is used to position a stage with {@link #show(double, double, Anchor)} + * or {@link #showAndWait(double, double, Anchor)}. The anchor is the point on the stage that should + * coincide with a given screen location. + *

+ * Anchors can be specified in one of two coordinate systems: + *

+ * + * @since 26 + */ + public static final class Anchor { + + private final double x; + private final double y; + private final boolean relative; + + private Anchor(double x, double y, boolean relative) { + this.x = x; + this.y = y; + this.relative = relative; + } + + /** + * Creates a relative anchor expressed as a fraction of the stage size. + * The values can be less than 0 or greater than 1; in this case the anchor is located outside the stage. + *

+ * {@code (0,0)} is the top-left; {@code (1,1)} is the bottom-right; {@code (0.5,0.5)} is the center. + * + * @param x x fraction of the stage width + * @param y y fraction of the stage height + * @return a relative {@code Anchor} + */ + public static Anchor ofRelative(double x, double y) { + return new Anchor(x, y, true); + } + + /** + * Creates an absolute anchor expressed as pixel offsets from the stage's top-left corner. + * The values can be less than 0 or greater than the stage's size; in this case the anchor + * is located outside the stage. + * + * @param x x offset in pixels from the stage's left edge + * @param y y offset in pixels from the stage's top edge + * @return an absolute {@code Anchor} + */ + public static Anchor ofAbsolute(double x, double y) { + return new Anchor(x, y, false); + } + + /** + * Gets the horizontal location of the anchor. + * + * @return the horizontal location of the anchor + */ + public double getX() { + return x; + } + + /** + * Gets the vertical location of the anchor. + * + * @return the vertical location of the anchor + */ + public double getY() { + return y; + } + + /** + * Returns whether the anchor is expressed as a fraction of the stage size. + * + * @return {@code true} if the anchor is expressed as a fraction of the stage size, + * {@code false} otherwise + */ + public boolean isRelative() { + return relative; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Anchor other + && other.x == x + && other.y == y + && other.relative == relative; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, relative); + } + } } diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java index 9f518cfe7ff..a947e69e241 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java @@ -1117,6 +1117,9 @@ public final ObjectProperty> onHiddenProperty() { 0, 0); } + // Give subclasses a chance to adjust the window bounds + fixBounds(); + // set peer bounds before the window is shown applyBounds(); @@ -1363,6 +1366,8 @@ private void focusChanged(final boolean newIsFocused) { } } + void fixBounds() {} + final void applyBounds() { peerBoundsConfigurator.apply(); } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 1607ec557dd..d3eea95ea03 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,7 @@ package test.javafx.stage; import java.util.ArrayList; +import java.util.stream.Stream; import javafx.scene.image.Image; import com.sun.javafx.stage.WindowHelper; import javafx.scene.Group; @@ -39,6 +40,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -55,9 +60,10 @@ public class StageTest { public void setUp() { toolkit = (StubToolkit) Toolkit.getToolkit(); s = new Stage(); - s.show(); - peer = (StubStage) WindowHelper.getPeer(s); - initialNumTimesSetSizeAndLocation = peer.numTimesSetSizeAndLocation; + s.setOnShown(_ -> { + peer = (StubStage) WindowHelper.getPeer(s); + initialNumTimesSetSizeAndLocation = peer.numTimesSetSizeAndLocation; + }); } @AfterEach @@ -75,6 +81,7 @@ private void pulse() { */ @Test public void testMovingStage() { + s.show(); s.setX(100); pulse(); assertEquals(100f, peer.x); @@ -88,6 +95,7 @@ public void testMovingStage() { */ @Test public void testResizingStage() { + s.show(); s.setWidth(100); s.setHeight(100); pulse(); @@ -103,6 +111,7 @@ public void testResizingStage() { */ @Test public void testMovingAndResizingStage() { + s.show(); s.setX(101); s.setY(102); s.setWidth(103); @@ -122,6 +131,7 @@ public void testMovingAndResizingStage() { */ @Test public void testResizingTooSmallStage() { + s.show(); s.setWidth(60); s.setHeight(70); s.setMinWidth(150); @@ -137,6 +147,7 @@ public void testResizingTooSmallStage() { */ @Test public void testResizingTooBigStage() { + s.show(); s.setWidth(100); s.setHeight(100); s.setMaxWidth(60); @@ -152,6 +163,7 @@ public void testResizingTooBigStage() { */ @Test public void testSizeAndLocationChangedOverTime() { + s.show(); pulse(); assertTrue((peer.numTimesSetSizeAndLocation - initialNumTimesSetSizeAndLocation) <= 1); initialNumTimesSetSizeAndLocation = peer.numTimesSetSizeAndLocation; @@ -176,6 +188,7 @@ public void testSizeAndLocationChangedOverTime() { @Test public void testSecondCenterOnScreenNotIgnored() { + s.show(); s.centerOnScreen(); s.setX(0); @@ -191,6 +204,7 @@ public void testSecondCenterOnScreenNotIgnored() { @Test public void testSecondSizeToSceneNotIgnored() { + s.show(); final Scene scene = new Scene(new Group(), 200, 100); s.setScene(scene); @@ -215,6 +229,7 @@ public void testCenterOnScreenForWindowOnSecondScreen() { 1920, 160, 1440, 900, 96)); try { + s.show(); s.setX(1920); s.setY(160); s.setWidth(300); @@ -238,6 +253,7 @@ public void testCenterOnScreenForOwnerOnSecondScreen() { 1920, 160, 1440, 900, 96)); try { + s.show(); s.setX(1920); s.setY(160); s.setWidth(300); @@ -260,6 +276,7 @@ public void testCenterOnScreenForOwnerOnSecondScreen() { @Test public void testSwitchSceneWithFixedSize() { + s.show(); Scene scene = new Scene(new Group(), 200, 100); s.setScene(scene); @@ -285,6 +302,7 @@ public void testSwitchSceneWithFixedSize() { @Test public void testSetBoundsNotLostForAsyncNotifications() { + s.show(); s.setX(20); s.setY(50); s.setWidth(400); @@ -309,6 +327,7 @@ public void testSetBoundsNotLostForAsyncNotifications() { @Test public void testFullscreenNotLostForAsyncNotifications() { + s.show(); peer.holdNotifications(); s.setFullScreen(true); @@ -327,6 +346,7 @@ public void testFullscreenNotLostForAsyncNotifications() { @Test public void testFullScreenNotification() { + s.show(); peer.setFullScreen(true); assertTrue(s.isFullScreen()); peer.setFullScreen(false); @@ -335,6 +355,7 @@ public void testFullScreenNotification() { @Test public void testResizableNotLostForAsyncNotifications() { + s.show(); peer.holdNotifications(); s.setResizable(false); @@ -353,6 +374,7 @@ public void testResizableNotLostForAsyncNotifications() { @Test public void testResizableNotification() { + s.show(); peer.setResizable(false); assertFalse(s.isResizable()); peer.setResizable(true); @@ -361,6 +383,7 @@ public void testResizableNotification() { @Test public void testIconifiedNotLostForAsyncNotifications() { + s.show(); peer.holdNotifications(); s.setIconified(true); @@ -379,6 +402,7 @@ public void testIconifiedNotLostForAsyncNotifications() { @Test public void testIconifiedNotification() { + s.show(); peer.setIconified(true); assertTrue(s.isIconified()); peer.setIconified(false); @@ -387,6 +411,7 @@ public void testIconifiedNotification() { @Test public void testMaximixedNotLostForAsyncNotifications() { + s.show(); peer.holdNotifications(); s.setMaximized(true); @@ -405,6 +430,7 @@ public void testMaximixedNotLostForAsyncNotifications() { @Test public void testMaximizedNotification() { + s.show(); peer.setMaximized(true); assertTrue(s.isMaximized()); peer.setMaximized(false); @@ -413,6 +439,7 @@ public void testMaximizedNotification() { @Test public void testAlwaysOnTopNotLostForAsyncNotifications() { + s.show(); peer.holdNotifications(); s.setAlwaysOnTop(true); @@ -431,6 +458,7 @@ public void testAlwaysOnTopNotLostForAsyncNotifications() { @Test public void testAlwaysOnTopNotification() { + s.show(); peer.setAlwaysOnTop(true); assertTrue(s.isAlwaysOnTop()); peer.setAlwaysOnTop(false); @@ -439,6 +467,7 @@ public void testAlwaysOnTopNotification() { @Test public void testBoundsSetAfterPeerIsRecreated() { + s.show(); s.setX(20); s.setY(50); s.setWidth(400); @@ -518,4 +547,172 @@ public void testAddAndSetNullIcon() { assertTrue(e instanceof NullPointerException, failMessage); } } + + /** + * Tests that a stage that is shown with an anchor and placed such that it extends slightly beyond + * the edges of the screen is repositioned so that it fits within the screen. + */ + @ParameterizedTest(name = "Clamps to {0} edges with {1} anchor") + @MethodSource("showWithAnchorClampsWindowToScreenEdges_arguments") + public void showWithAnchorClampsWindowToScreenEdges( + @SuppressWarnings("unused") String edge, + @SuppressWarnings("unused") String anchorName, + Stage.Anchor anchor, + double screenW, double screenH, + double stageW, double stageH, + double requestX, double requestY) { + toolkit.setScreens( + new ScreenConfiguration( + 0, 0, (int)screenW, (int)screenH, + 0, 0, (int)screenW, (int)screenH, + 96)); + + try { + s.setWidth(stageW); + s.setHeight(stageH); + s.show(requestX, requestY, anchor); + pulse(); + + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + } finally { + toolkit.resetScreens(); + } + } + + private static Stream showWithAnchorClampsWindowToScreenEdges_arguments() { + final double screenW = 800; + final double screenH = 600; + final double stageW = 200; + final double stageH = 200; + final double overshoot = 10; // push past the edge to force clamping + + Stream.Builder b = Stream.builder(); + b.add(Arguments.of("bottom and right", "top left", Stage.Anchor.ofRelative(0, 0), screenW, screenH, + stageW, stageH, screenW - stageW - overshoot, screenH - stageH - overshoot)); + b.add(Arguments.of("bottom and left", "top right", Stage.Anchor.ofRelative(0, 1), screenW, screenH, + stageW, stageH, screenW - stageW - overshoot, stageH - overshoot)); + b.add(Arguments.of("top and right", "bottom left", Stage.Anchor.ofRelative(1, 0), screenW, screenH, + stageW, stageH, stageW - overshoot, screenH - stageH - overshoot)); + b.add(Arguments.of("top and left", "bottom right", Stage.Anchor.ofRelative(1, 1), screenW, screenH, + stageW, stageH, stageW - overshoot, stageH - overshoot)); + return b.build(); + } + + @Test + public void showWithAbsoluteAnchorNoClamping() { + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + + try { + s.setWidth(200); + s.setHeight(100); + s.show(50, 70, Stage.Anchor.ofAbsolute(0, 0)); + pulse(); + + assertTrue(s.isShowing()); + assertEquals(50, peer.x, 0.0001); + assertEquals(70, peer.y, 0.0001); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + } finally { + toolkit.resetScreens(); + } + } + + @Test + public void showWithRelativeCenterAnchor() { + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + + try { + s.setWidth(200); + s.setHeight(100); + s.show(400, 300, Stage.Anchor.ofRelative(0.5, 0.5)); + pulse(); + + assertEquals(300, peer.x, 0.0001); + assertEquals(250, peer.y, 0.0001); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + } finally { + toolkit.resetScreens(); + } + } + + @Test + public void showWithRelativeCenterAnchorClampsToEdges() { + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + + try { + s.setWidth(200); + s.setHeight(100); + + // Center at x=790 would imply top-left x=690, which would extend past the right edge (800) + s.show(790, 100, Stage.Anchor.ofRelative(0.5, 0.5)); + pulse(); + + // Clamped to x=800-200=600, y stays as computed (100-50=50) since it's in range + assertEquals(600, peer.x, 0.0001); + assertEquals(50, peer.y, 0.0001); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + } finally { + toolkit.resetScreens(); + } + } + + @Test + public void showWithAnchorMovesStageWhenAlreadyShowing() { + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + + try { + s.setWidth(200); + s.setHeight(100); + + s.show(10, 10, Stage.Anchor.ofAbsolute(0, 0)); + pulse(); + assertTrue(s.isShowing()); + assertEquals(10, peer.x, 0.0001); + assertEquals(10, peer.y, 0.0001); + + // Calling show again should reposition + s.show(120, 140, Stage.Anchor.ofAbsolute(0, 0)); + pulse(); + + assertTrue(s.isShowing()); + assertEquals(120, peer.x, 0.0001); + assertEquals(140, peer.y, 0.0001); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + } finally { + toolkit.resetScreens(); + } + } + + @Test + public void showWithAnchorOnSecondScreenUsesSecondScreenBoundsForClamping() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 1920, 1200, 0, 0, 1920, 1172, 96), + new ScreenConfiguration(1920, 160, 1440, 900, 1920, 160, 1440, 900, 96)); + + try { + s.setWidth(400); + s.setHeight(300); + + // Request a position inside the second screen, but would overflow its right/bottom edges. + double requestX = 1920 + 1440 - 10; + double requestY = 160 + 900 - 10; + + s.show(requestX, requestY, Stage.Anchor.ofAbsolute(0, 0)); + pulse(); + + // Expected clamp against *second* screen bounds: + assertEquals(2960, peer.x, 0.0001); + assertEquals(760, peer.y, 0.0001); + assertWithinScreenBounds(peer, toolkit.getScreens().get(1)); + } finally { + toolkit.resetScreens(); + } + } + + private static void assertWithinScreenBounds(StubStage peer, ScreenConfiguration screen) { + assertTrue(screen.getMinX() <= peer.x); + assertTrue(screen.getMinY() <= peer.y); + assertTrue(screen.getMinX() + screen.getWidth() >= peer.x + peer.width); + assertTrue(screen.getMinY() + screen.getHeight() >= peer.y + peer.height); + } } From 3789271ee74f90fdbfa4f7739757848fb60fa553 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:30:45 +0100 Subject: [PATCH 02/12] refactor Anchor -> AnchorPoint --- .../java/javafx/geometry/AnchorPoint.java | 188 ++++++++++++++++++ .../src/main/java/javafx/stage/Stage.java | 130 ++---------- 2 files changed, 207 insertions(+), 111 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java diff --git a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java new file mode 100644 index 00000000000..172295d27a9 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.geometry; + +/** + * Represents a reference point within a target area, used for anchoring geometry-dependent calculations + * such as positioning a window relative to another location. + *

+ * An {@code AnchorPoint} provides a {@code (x, y)} coordinate together with a flag indicating + * how those coordinates should be interpreted: + *

+ * + * @since 26 + */ +public final class AnchorPoint { + + private final double x; + private final double y; + private final boolean proportional; + + private AnchorPoint(double x, double y, boolean proportional) { + this.x = x; + this.y = y; + this.proportional = proportional; + } + + /** + * Creates a proportional anchor point, expressed as fractions of the target area's width and height. + *

+ * In proportional coordinates, {@code (0, 0)} refers to the top-left corner of the target area and + * {@code (1, 1)} refers to the bottom-right corner. Values outside the {@code [0..1]} range represent + * points outside the bounds. + * + * @param x the horizontal fraction of the target width + * @param y the vertical fraction of the target height + * @return a proportional {@code AnchorPoint} + */ + public static AnchorPoint proportional(double x, double y) { + return new AnchorPoint(x, y, true); + } + + /** + * Creates an absolute anchor point, expressed as pixel offsets from the top-left corner of the target area. + * + * @param x the horizontal offset in pixels from the left edge of the target area + * @param y the vertical offset in pixels from the top edge of the target area + * @return an absolute {@code AnchorPoint} + */ + public static AnchorPoint absolute(double x, double y) { + return new AnchorPoint(x, y, false); + } + + /** + * Returns the horizontal coordinate of this anchor point. + * + * @return the horizontal coordinate of this anchor point + */ + public double getX() { + return x; + } + + /** + * Returns the vertical coordinate of this anchor point. + * + * @return the vertical coordinate of this anchor point + */ + public double getY() { + return y; + } + + /** + * Indicates whether the {@code x} and {@code y} coordinates are proportional to the size of the target area. + * + * @return {@code true} if the coordinates are proportional, {@code false} otherwise + */ + public boolean isProportional() { + return proportional; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AnchorPoint other + && x == other.x + && y == other.y + && proportional == other.proportional; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + Double.hashCode(this.x); + hash = 37 * hash + Double.hashCode(this.y); + hash = 37 * hash + Boolean.hashCode(this.proportional); + return hash; + } + + /** + * Anchor at the top-left corner of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0, 0)}. + */ + public static final AnchorPoint TOP_LEFT = new AnchorPoint(0, 0, true); + + /** + * Anchor at the top-center midpoint of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0)}. + */ + public static final AnchorPoint TOP_CENTER = new AnchorPoint(0.5, 0, true); + + /** + * Anchor at the top-right corner of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(1, 0)}. + */ + public static final AnchorPoint TOP_RIGHT = new AnchorPoint(1, 0, true); + + /** + * Anchor at the center-left midpoint of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0, 0.5)}. + */ + public static final AnchorPoint CENTER_LEFT = new AnchorPoint(0, 0.5, true); + + /** + * Anchor at the center of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0.5)}. + */ + public static final AnchorPoint CENTER = new AnchorPoint(0.5, 0.5, true); + + /** + * Anchor at the center-right midpoint of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(1, 0.5)}. + */ + public static final AnchorPoint CENTER_RIGHT = new AnchorPoint(1, 0.5, true); + + /** + * Anchor at the bottom-left corner of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0, 1)}. + */ + public static final AnchorPoint BOTTOM_LEFT = new AnchorPoint(0, 1, true); + + /** + * Anchor at the bottom-center midpoint of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 1)}. + */ + public static final AnchorPoint BOTTOM_CENTER = new AnchorPoint(0.5, 1, true); + + /** + * Anchor at the bottom-right corner of the target area. + *

+ * This constant is equivalent to {@code AnchorPoint.proportional(1, 1)}. + */ + public static final AnchorPoint BOTTOM_RIGHT = new AnchorPoint(1, 1, true); +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 3e8b48895b4..90f2ebc6e4d 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -37,6 +36,7 @@ import javafx.beans.property.StringPropertyBase; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; +import javafx.geometry.AnchorPoint; import javafx.geometry.NodeOrientation; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; @@ -318,7 +318,7 @@ public Stage(@NamedArg(value="style", defaultValue="DECORATED") StageStyle style * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen * @since 26 */ - public final void show(double anchorX, double anchorY, Anchor anchor) { + public final void show(double anchorX, double anchorY, AnchorPoint anchor) { if (isShowing()) { new PositionRequest(anchorX, anchorY, anchor).apply(this); } else { @@ -492,7 +492,7 @@ public void showAndWait() { * @throws IllegalStateException if this stage is already showing * @since 26 */ - public final void showAndWait(double anchorX, double anchorY, Anchor anchor) { + public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor) { verifyCanShowAndWait(); positionRequest = new PositionRequest(anchorX, anchorY, anchor); super.show(); @@ -1391,7 +1391,13 @@ final void fixBounds() { private PositionRequest positionRequest; - private record PositionRequest(double screenX, double screenY, Anchor anchor) { + private record PositionRequest(double screenX, double screenY, AnchorPoint anchor) { + + PositionRequest { + if (anchor == null) { + throw new NullPointerException("anchor cannot be null"); + } + } void apply(Stage stage) { Screen currentScreen = Utils.getScreenForPoint(screenX, screenY); @@ -1404,16 +1410,16 @@ void apply(Stage stage) { double anchorX, anchorY; double anchorRelX, anchorRelY; - if (anchor.relative) { - anchorX = width * anchor.x; - anchorY = height * anchor.y; - anchorRelX = anchor.x; - anchorRelY = anchor.y; + if (anchor.isProportional()) { + anchorX = width * anchor.getX(); + anchorY = height * anchor.getY(); + anchorRelX = anchor.getX(); + anchorRelY = anchor.getY(); } else { - anchorX = anchor.x; - anchorY = anchor.y; - anchorRelX = anchor.x / width; - anchorRelY = anchor.y / height; + anchorX = anchor.getX(); + anchorY = anchor.getY(); + anchorRelX = anchor.getX() / width; + anchorRelY = anchor.getY() / height; } double minX = screenBounds.getMinX(); @@ -1438,102 +1444,4 @@ void apply(Stage stage) { stage.setY(y); } } - - /** - * Defines an anchor point that is used to position a stage with {@link #show(double, double, Anchor)} - * or {@link #showAndWait(double, double, Anchor)}. The anchor is the point on the stage that should - * coincide with a given screen location. - *

- * Anchors can be specified in one of two coordinate systems: - *

- * - * @since 26 - */ - public static final class Anchor { - - private final double x; - private final double y; - private final boolean relative; - - private Anchor(double x, double y, boolean relative) { - this.x = x; - this.y = y; - this.relative = relative; - } - - /** - * Creates a relative anchor expressed as a fraction of the stage size. - * The values can be less than 0 or greater than 1; in this case the anchor is located outside the stage. - *

- * {@code (0,0)} is the top-left; {@code (1,1)} is the bottom-right; {@code (0.5,0.5)} is the center. - * - * @param x x fraction of the stage width - * @param y y fraction of the stage height - * @return a relative {@code Anchor} - */ - public static Anchor ofRelative(double x, double y) { - return new Anchor(x, y, true); - } - - /** - * Creates an absolute anchor expressed as pixel offsets from the stage's top-left corner. - * The values can be less than 0 or greater than the stage's size; in this case the anchor - * is located outside the stage. - * - * @param x x offset in pixels from the stage's left edge - * @param y y offset in pixels from the stage's top edge - * @return an absolute {@code Anchor} - */ - public static Anchor ofAbsolute(double x, double y) { - return new Anchor(x, y, false); - } - - /** - * Gets the horizontal location of the anchor. - * - * @return the horizontal location of the anchor - */ - public double getX() { - return x; - } - - /** - * Gets the vertical location of the anchor. - * - * @return the vertical location of the anchor - */ - public double getY() { - return y; - } - - /** - * Returns whether the anchor is expressed as a fraction of the stage size. - * - * @return {@code true} if the anchor is expressed as a fraction of the stage size, - * {@code false} otherwise - */ - public boolean isRelative() { - return relative; - } - - @Override - public boolean equals(Object obj) { - return obj instanceof Anchor other - && other.x == x - && other.y == y - && other.relative == relative; - } - - @Override - public int hashCode() { - return Objects.hash(x, y, relative); - } - } } From d44562346b641efd632e6f58f1afe07bb05725a4 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:33:28 +0100 Subject: [PATCH 03/12] update tests --- .../java/test/javafx/stage/StageTest.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index d3eea95ea03..8872d273367 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -27,15 +27,16 @@ import java.util.ArrayList; import java.util.stream.Stream; +import javafx.geometry.AnchorPoint; import javafx.scene.image.Image; -import com.sun.javafx.stage.WindowHelper; import javafx.scene.Group; import javafx.scene.Scene; +import javafx.stage.Stage; import test.com.sun.javafx.pgstub.StubStage; import test.com.sun.javafx.pgstub.StubToolkit; import test.com.sun.javafx.pgstub.StubToolkit.ScreenConfiguration; +import com.sun.javafx.stage.WindowHelper; import com.sun.javafx.tk.Toolkit; -import javafx.stage.Stage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -557,7 +558,7 @@ public void testAddAndSetNullIcon() { public void showWithAnchorClampsWindowToScreenEdges( @SuppressWarnings("unused") String edge, @SuppressWarnings("unused") String anchorName, - Stage.Anchor anchor, + AnchorPoint anchor, double screenW, double screenH, double stageW, double stageH, double requestX, double requestY) { @@ -587,13 +588,13 @@ private static Stream showWithAnchorClampsWindowToScreenEdges_argumen final double overshoot = 10; // push past the edge to force clamping Stream.Builder b = Stream.builder(); - b.add(Arguments.of("bottom and right", "top left", Stage.Anchor.ofRelative(0, 0), screenW, screenH, + b.add(Arguments.of("bottom and right", "top left", AnchorPoint.TOP_LEFT, screenW, screenH, stageW, stageH, screenW - stageW - overshoot, screenH - stageH - overshoot)); - b.add(Arguments.of("bottom and left", "top right", Stage.Anchor.ofRelative(0, 1), screenW, screenH, + b.add(Arguments.of("bottom and left", "top right", AnchorPoint.TOP_RIGHT, screenW, screenH, stageW, stageH, screenW - stageW - overshoot, stageH - overshoot)); - b.add(Arguments.of("top and right", "bottom left", Stage.Anchor.ofRelative(1, 0), screenW, screenH, + b.add(Arguments.of("top and right", "bottom left", AnchorPoint.BOTTOM_LEFT, screenW, screenH, stageW, stageH, stageW - overshoot, screenH - stageH - overshoot)); - b.add(Arguments.of("top and left", "bottom right", Stage.Anchor.ofRelative(1, 1), screenW, screenH, + b.add(Arguments.of("top and left", "bottom right", AnchorPoint.BOTTOM_RIGHT, screenW, screenH, stageW, stageH, stageW - overshoot, stageH - overshoot)); return b.build(); } @@ -605,7 +606,7 @@ public void showWithAbsoluteAnchorNoClamping() { try { s.setWidth(200); s.setHeight(100); - s.show(50, 70, Stage.Anchor.ofAbsolute(0, 0)); + s.show(50, 70, AnchorPoint.absolute(0, 0)); pulse(); assertTrue(s.isShowing()); @@ -624,7 +625,7 @@ public void showWithRelativeCenterAnchor() { try { s.setWidth(200); s.setHeight(100); - s.show(400, 300, Stage.Anchor.ofRelative(0.5, 0.5)); + s.show(400, 300, AnchorPoint.proportional(0.5, 0.5)); pulse(); assertEquals(300, peer.x, 0.0001); @@ -644,7 +645,7 @@ public void showWithRelativeCenterAnchorClampsToEdges() { s.setHeight(100); // Center at x=790 would imply top-left x=690, which would extend past the right edge (800) - s.show(790, 100, Stage.Anchor.ofRelative(0.5, 0.5)); + s.show(790, 100, AnchorPoint.proportional(0.5, 0.5)); pulse(); // Clamped to x=800-200=600, y stays as computed (100-50=50) since it's in range @@ -664,14 +665,14 @@ public void showWithAnchorMovesStageWhenAlreadyShowing() { s.setWidth(200); s.setHeight(100); - s.show(10, 10, Stage.Anchor.ofAbsolute(0, 0)); + s.show(10, 10, AnchorPoint.absolute(0, 0)); pulse(); assertTrue(s.isShowing()); assertEquals(10, peer.x, 0.0001); assertEquals(10, peer.y, 0.0001); // Calling show again should reposition - s.show(120, 140, Stage.Anchor.ofAbsolute(0, 0)); + s.show(120, 140, AnchorPoint.absolute(0, 0)); pulse(); assertTrue(s.isShowing()); @@ -697,7 +698,7 @@ public void showWithAnchorOnSecondScreenUsesSecondScreenBoundsForClamping() { double requestX = 1920 + 1440 - 10; double requestY = 160 + 900 - 10; - s.show(requestX, requestY, Stage.Anchor.ofAbsolute(0, 0)); + s.show(requestX, requestY, AnchorPoint.absolute(0, 0)); pulse(); // Expected clamp against *second* screen bounds: From 1e4149ea1b405d397567dcb4d3882d0e3fe56219 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:28:39 +0100 Subject: [PATCH 04/12] add clampPolicy and screenPadding --- .../main/java/javafx/stage/ClampPolicy.java | 69 +++++++ .../src/main/java/javafx/stage/Stage.java | 178 ++++++++++++++---- .../java/test/javafx/stage/StageTest.java | 139 +++++++------- 3 files changed, 287 insertions(+), 99 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java new file mode 100644 index 00000000000..7a921642d18 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.stage; + +/** + * Specifies how a {@link Stage} should be clamped to the screen bounds. + *

+ * Clamping adjusts the computed window position so that the window is shown within the screen bounds. + * Clamping can be applied independently on the horizontal axis, the vertical axis, both axes, or not at all. + * + * @since 26 + */ +public enum ClampPolicy { + + /** + * Do not clamp the computed position. + *

+ * The window is placed exactly as specified by the requested screen coordinates, even if this + * causes parts of the window to extend beyond the bounds of the screen. + */ + NONE, + + /** + * Clamp the computed position horizontally only. + *

+ * The {@code x} coordinate of the window is adjusted as needed to keep the window within the screen + * bounds, while the {@code y} coordinate is left unchanged. + */ + HORIZONTAL, + + /** + * Clamp the computed position vertically only. + *

+ * The {@code y} coordinate of the window is adjusted as needed to keep the window within the screen + * bounds, while the {@code x} coordinate is left unchanged. + */ + VERTICAL, + + /** + * Clamp the computed position both horizontally and vertically. + *

+ * Both the {@code x} and {@code y} coordinates of the window are adjusted as needed to keep the + * window within the screen bounds. + */ + BOTH +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 90f2ebc6e4d..ef1db2427a6 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -37,6 +38,7 @@ import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.geometry.AnchorPoint; +import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; @@ -304,25 +306,67 @@ public Stage(@NamedArg(value="style", defaultValue="DECORATED") StageStyle style * Shows this stage at the specified location and adjusts the position as needed to keep the stage * visible on screen. If the stage is already showing, it is moved to the computed position instead. *

- * Positioning is done using an {@code anchor} point on the stage. The specified location is interpreted - * as the desired screen coordinates of that anchor, and the stage location is derived from that. - * For example, if the anchor is {@code (0.5, 0.5)}, the stage is positioned so its center lies at - * {@code (anchorX, anchorY)}; if the anchor is {@code (0, 0)}, the stage's top-left corner is placed - * at {@code (anchorX, anchorY)}. The final stage location is clamped to the screen bounds. + * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the + * stage size, and is then adjusted to keep the stage within the screen bounds. *

* After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will * reflect the adjusted position. + *

+ * Calling this method is equivalent to calling + * {@code show(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY)}. * * @param anchorX the requested horizontal location of the anchor point on the screen * @param anchorY the requested vertical location of the anchor point on the screen * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @throws NullPointerException if {@code anchor} is {@code null} * @since 26 */ public final void show(double anchorX, double anchorY, AnchorPoint anchor) { + show(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY); + } + + /** + * Shows this stage at the specified location using the given anchor and clamping options. + * If the stage is already showing, it is moved to the computed position instead. + *

+ * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the + * stage size, and is then adjusted according to {@code clampPolicy}. + *

+ * The {@code clampPolicy} controls which axes are clamped to the screen bounds: + *

+ * If clamping is performed, {@code screenPadding} specifies additional space to maintain between the + * stage edges and the screen edges. The padding is applied per edge (top/right/bottom/left) and + * effectively shrinks the usable screen area for clamping by the given insets. For example, a left + * padding of {@code 10} ensures that, after clamping, the stage will not be placed closer than + * 10 pixels to the left screen edge (and similarly for the other edges). + *

+ * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will + * reflect the adjusted position. + * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @param clampPolicy controls whether clamping is performed horizontally, vertically, both, or not at all + * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges + * when clamping is performed + * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} + * @since 26 + */ + public final void show(double anchorX, double anchorY, AnchorPoint anchor, + ClampPolicy clampPolicy, Insets screenPadding) { + var positionRequest = new PositionRequest(anchorX, anchorY, anchor, clampPolicy, screenPadding); + if (isShowing()) { - new PositionRequest(anchorX, anchorY, anchor).apply(this); + positionRequest.apply(this); } else { - positionRequest = new PositionRequest(anchorX, anchorY, anchor); + this.positionRequest = positionRequest; super.show(); } } @@ -470,21 +514,23 @@ public void showAndWait() { /** * Shows this stage at the specified location and adjusts the position as needed to keep the stage * visible on screen. This method blocks until the stage is hidden before returning to the caller. - * This method temporarily blocks processing of the current event and starts a nested event loop - * to handle other events. This method must be called on the JavaFX application thread. + * It also temporarily blocks processing of the current event and starts a nested event loop to + * handle other events. This method must be called on the JavaFX application thread. *

- * Positioning is done using an {@code anchor} point on the stage. The specified location is interpreted - * as the desired screen coordinates of that anchor, and the stage location is derived from that. - * For example, if the anchor is {@code (0.5, 0.5)}, the stage is positioned so its center lies at - * {@code (anchorX, anchorY)}; if the anchor is {@code (0, 0)}, the stage's top-left corner is placed - * at {@code (anchorX, anchorY)}. The final stage location is clamped to the screen bounds. + * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the + * stage size, and is then adjusted to keep the stage within the screen bounds. *

* After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will * reflect the adjusted position. + *

+ * Calling this method is equivalent to calling + * {@code showAndWait(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY)}. * * @param anchorX the requested horizontal location of the anchor point on the screen * @param anchorY the requested vertical location of the anchor point on the screen * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @throws NullPointerException if {@code anchor} is {@code null} * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread * @throws IllegalStateException if this method is called during animation or layout processing * @throws IllegalStateException if this call would exceed the maximum number of nested event loops @@ -493,8 +539,53 @@ public void showAndWait() { * @since 26 */ public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor) { + showAndWait(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY); + } + + /** + * Shows this stage at the specified location using the given anchor and clamping options. + * This method blocks until the stage is hidden before returning to the caller. + * It also temporarily blocks processing of the current event and starts a nested event loop to + * handle other events. This method must be called on the JavaFX application thread. + *

+ * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the + * stage size, and is then adjusted according to {@code clampPolicy}. + *

+ * The {@code clampPolicy} controls which axes are clamped to the screen bounds: + *

+ * If clamping is performed, {@code screenPadding} specifies additional space to maintain between the + * stage edges and the screen edges. The padding is applied per edge (top/right/bottom/left) and + * effectively shrinks the usable screen area for clamping by the given insets. For example, a left + * padding of {@code 10} ensures that, after clamping, the stage will not be placed closer than + * 10 pixels to the left screen edge (and similarly for the other edges). + *

+ * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will + * reflect the adjusted position. + * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @param clampPolicy controls whether clamping is performed horizontally, vertically, both, or not at all + * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges + * when clamping is performed + * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} + * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread + * @throws IllegalStateException if this method is called during animation or layout processing + * @throws IllegalStateException if this call would exceed the maximum number of nested event loops + * @throws IllegalStateException if this method is called on the primary stage + * @throws IllegalStateException if this stage is already showing + * @since 26 + */ + public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor, + ClampPolicy clampPolicy, Insets screenPadding) { verifyCanShowAndWait(); - positionRequest = new PositionRequest(anchorX, anchorY, anchor); + positionRequest = new PositionRequest(anchorX, anchorY, anchor, clampPolicy, screenPadding); super.show(); inNestedEventLoop = true; Toolkit.getToolkit().enterNestedEventLoop(this); @@ -1391,12 +1482,13 @@ final void fixBounds() { private PositionRequest positionRequest; - private record PositionRequest(double screenX, double screenY, AnchorPoint anchor) { + private record PositionRequest(double screenX, double screenY, AnchorPoint anchor, + ClampPolicy clampPolicy, Insets screenPadding) { PositionRequest { - if (anchor == null) { - throw new NullPointerException("anchor cannot be null"); - } + Objects.requireNonNull(anchor, "anchor cannot be null"); + Objects.requireNonNull(clampPolicy, "clampPolicy cannot be null"); + Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); } void apply(Stage stage) { @@ -1418,26 +1510,44 @@ void apply(Stage stage) { } else { anchorX = anchor.getX(); anchorY = anchor.getY(); - anchorRelX = anchor.getX() / width; - anchorRelY = anchor.getY() / height; + anchorRelX = width != 0 ? anchor.getX() / width : 0; + anchorRelY = height != 0 ? anchor.getY() / height : 0; } - double minX = screenBounds.getMinX(); - double minY = screenBounds.getMinY(); - double maxX = screenBounds.getMaxX() - width; - double maxY = screenBounds.getMaxY() - height; - double x, y; + // Raw (unclamped) top-left position derived from the requested screen point + anchor + double rawX = screenX - anchorX; + double rawY = screenY - anchorY; - if (maxX >= minX) { - x = Utils.clamp(minX, screenX - anchorX, maxX); - } else { - x = anchorRelX > 0.5 ? maxX : minX; + // Start with raw coordinates; clamp per policy below + double x = rawX; + double y = rawY; + + // Only compute clamp ranges for axes that are being clamped + boolean clampH = clampPolicy == ClampPolicy.BOTH || clampPolicy == ClampPolicy.HORIZONTAL; + boolean clampV = clampPolicy == ClampPolicy.BOTH || clampPolicy == ClampPolicy.VERTICAL; + + if (clampH) { + double minX = screenBounds.getMinX() + screenPadding.getLeft(); + double maxX = screenBounds.getMaxX() - screenPadding.getRight() - width; + + if (maxX >= minX) { + x = Utils.clamp(minX, rawX, maxX); + } else { + // Window (plus padding) doesn't fit horizontally: pick a side based on anchor + x = anchorRelX > 0.5 ? maxX : minX; + } } - if (maxY >= minY) { - y = Utils.clamp(minY, screenY - anchorY, maxY); - } else { - y = anchorRelY > 0.5 ? maxY : minY; + if (clampV) { + double minY = screenBounds.getMinY() + screenPadding.getTop(); + double maxY = screenBounds.getMaxY() - screenPadding.getBottom() - height; + + if (maxY >= minY) { + y = Utils.clamp(minY, rawY, maxY); + } else { + // Window (plus padding) doesn't fit vertically: pick a side based on anchor + y = anchorRelY > 0.5 ? maxY : minY; + } } stage.setX(x); diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 8872d273367..161fca102d1 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -28,9 +28,11 @@ import java.util.ArrayList; import java.util.stream.Stream; import javafx.geometry.AnchorPoint; +import javafx.geometry.Insets; import javafx.scene.image.Image; import javafx.scene.Group; import javafx.scene.Scene; +import javafx.stage.ClampPolicy; import javafx.stage.Stage; import test.com.sun.javafx.pgstub.StubStage; import test.com.sun.javafx.pgstub.StubToolkit; @@ -43,6 +45,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -551,14 +554,17 @@ public void testAddAndSetNullIcon() { /** * Tests that a stage that is shown with an anchor and placed such that it extends slightly beyond - * the edges of the screen is repositioned so that it fits within the screen. + * the edges of the screen is repositioned so that it fits within the screen, taking into account + * padding around the screen. */ @ParameterizedTest(name = "Clamps to {0} edges with {1} anchor") @MethodSource("showWithAnchorClampsWindowToScreenEdges_arguments") - public void showWithAnchorClampsWindowToScreenEdges( + public void showAndClampToScreenEdgesWithPadding( @SuppressWarnings("unused") String edge, @SuppressWarnings("unused") String anchorName, AnchorPoint anchor, + ClampPolicy clampPolicy, + Insets screenPadding, double screenW, double screenH, double stageW, double stageH, double requestX, double requestY) { @@ -571,10 +577,10 @@ public void showWithAnchorClampsWindowToScreenEdges( try { s.setWidth(stageW); s.setHeight(stageH); - s.show(requestX, requestY, anchor); + s.show(requestX, requestY, anchor, clampPolicy, screenPadding); pulse(); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), screenPadding); } finally { toolkit.resetScreens(); } @@ -588,70 +594,65 @@ private static Stream showWithAnchorClampsWindowToScreenEdges_argumen final double overshoot = 10; // push past the edge to force clamping Stream.Builder b = Stream.builder(); - b.add(Arguments.of("bottom and right", "top left", AnchorPoint.TOP_LEFT, screenW, screenH, - stageW, stageH, screenW - stageW - overshoot, screenH - stageH - overshoot)); - b.add(Arguments.of("bottom and left", "top right", AnchorPoint.TOP_RIGHT, screenW, screenH, - stageW, stageH, screenW - stageW - overshoot, stageH - overshoot)); - b.add(Arguments.of("top and right", "bottom left", AnchorPoint.BOTTOM_LEFT, screenW, screenH, - stageW, stageH, stageW - overshoot, screenH - stageH - overshoot)); - b.add(Arguments.of("top and left", "bottom right", AnchorPoint.BOTTOM_RIGHT, screenW, screenH, - stageW, stageH, stageW - overshoot, stageH - overshoot)); + + // no screen padding + b.add(Arguments.of("bottom and right", "TOP_LEFT", AnchorPoint.TOP_LEFT, + ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, + stageW, stageH, screenW - stageW + overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of("bottom and left", "TOP_RIGHT", AnchorPoint.TOP_RIGHT, + ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, + stageW, stageH, stageW - overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of("top and right", "BOTTOM_LEFT", AnchorPoint.BOTTOM_LEFT, + ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, + stageW, stageH, screenW - stageW + overshoot, stageH - overshoot)); + b.add(Arguments.of("top and left", "BOTTOM_RIGHT", AnchorPoint.BOTTOM_RIGHT, + ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, + stageW, stageH, stageW - overshoot, stageH - overshoot)); + + // with screen padding + b.add(Arguments.of("bottom and right", "TOP_LEFT", AnchorPoint.TOP_LEFT, + ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, + stageW, stageH, screenW - stageW + overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of("bottom and left", "TOP_RIGHT", AnchorPoint.TOP_RIGHT, + ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, + stageW, stageH, stageW - overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of("top and right", "BOTTOM_LEFT", AnchorPoint.BOTTOM_LEFT, + ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, + stageW, stageH, screenW - stageW + overshoot, stageH - overshoot)); + b.add(Arguments.of("top and left", "BOTTOM_RIGHT", AnchorPoint.BOTTOM_RIGHT, + ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, + stageW, stageH, stageW - overshoot, stageH - overshoot)); return b.build(); } - @Test - public void showWithAbsoluteAnchorNoClamping() { + /** + * Tests that the {@link ClampPolicy} is taken into account when clamping to screen edges. + */ + @ParameterizedTest + @CsvSource({ + "NONE, 790, 590", + "HORIZONTAL, 600, 590", + "VERTICAL, 790, 500", + "BOTH, 600, 500", + }) + public void showWithClampPolicy(ClampPolicy clampPolicy, double expectX, double expectY) { toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); try { s.setWidth(200); s.setHeight(100); - s.show(50, 70, AnchorPoint.absolute(0, 0)); + s.show(790, 590, AnchorPoint.absolute(0, 0), clampPolicy, Insets.EMPTY); pulse(); assertTrue(s.isShowing()); - assertEquals(50, peer.x, 0.0001); - assertEquals(70, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); - } finally { - toolkit.resetScreens(); - } - } - - @Test - public void showWithRelativeCenterAnchor() { - toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); - - try { - s.setWidth(200); - s.setHeight(100); - s.show(400, 300, AnchorPoint.proportional(0.5, 0.5)); - pulse(); - - assertEquals(300, peer.x, 0.0001); - assertEquals(250, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); - } finally { - toolkit.resetScreens(); - } - } - - @Test - public void showWithRelativeCenterAnchorClampsToEdges() { - toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); - - try { - s.setWidth(200); - s.setHeight(100); - - // Center at x=790 would imply top-left x=690, which would extend past the right edge (800) - s.show(790, 100, AnchorPoint.proportional(0.5, 0.5)); - pulse(); - - // Clamped to x=800-200=600, y stays as computed (100-50=50) since it's in range - assertEquals(600, peer.x, 0.0001); - assertEquals(50, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + assertEquals(expectX, peer.x, 0.0001); + assertEquals(expectY, peer.y, 0.0001); + + if (clampPolicy != ClampPolicy.BOTH) { + assertNotWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } else { + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } } finally { toolkit.resetScreens(); } @@ -678,14 +679,14 @@ public void showWithAnchorMovesStageWhenAlreadyShowing() { assertTrue(s.isShowing()); assertEquals(120, peer.x, 0.0001); assertEquals(140, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst()); + assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } finally { toolkit.resetScreens(); } } @Test - public void showWithAnchorOnSecondScreenUsesSecondScreenBoundsForClamping() { + public void showOnSecondScreenUsesSecondScreenBoundsForClamping() { toolkit.setScreens( new ScreenConfiguration(0, 0, 1920, 1200, 0, 0, 1920, 1172, 96), new ScreenConfiguration(1920, 160, 1440, 900, 1920, 160, 1440, 900, 96)); @@ -704,16 +705,24 @@ public void showWithAnchorOnSecondScreenUsesSecondScreenBoundsForClamping() { // Expected clamp against *second* screen bounds: assertEquals(2960, peer.x, 0.0001); assertEquals(760, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().get(1)); + assertWithinScreenBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); } finally { toolkit.resetScreens(); } } - private static void assertWithinScreenBounds(StubStage peer, ScreenConfiguration screen) { - assertTrue(screen.getMinX() <= peer.x); - assertTrue(screen.getMinY() <= peer.y); - assertTrue(screen.getMinX() + screen.getWidth() >= peer.x + peer.width); - assertTrue(screen.getMinY() + screen.getHeight() >= peer.y + peer.height); + private static void assertWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + assertTrue(isWithinScreenBounds(peer, screen, padding), "Stage is not within screen bounds"); + } + + private static void assertNotWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + assertFalse(isWithinScreenBounds(peer, screen, padding), "Stage is within screen bounds"); + } + + private static boolean isWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + return screen.getMinX() + padding.getLeft() <= peer.x + && screen.getMinY() + padding.getTop() <= peer.y + && screen.getMinX() + screen.getWidth() - padding.getRight() >= peer.x + peer.width + && screen.getMinY() + screen.getHeight() - padding.getBottom() >= peer.y + peer.height; } } From 6cf52e9204dbf616224679c23f9d14918059a33a Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:41:00 +0100 Subject: [PATCH 05/12] ensure screenPadding is not negative --- .../javafx.graphics/src/main/java/javafx/stage/Stage.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index ef1db2427a6..85b3012d71c 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -1489,6 +1489,13 @@ private record PositionRequest(double screenX, double screenY, AnchorPoint ancho Objects.requireNonNull(anchor, "anchor cannot be null"); Objects.requireNonNull(clampPolicy, "clampPolicy cannot be null"); Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); + + if (screenPadding.getTop() < 0 + || screenPadding.getRight() < 0 + || screenPadding.getBottom() < 0 + || screenPadding.getLeft() < 0) { + throw new IllegalArgumentException("screenPadding cannot be negative"); + } } void apply(Stage stage) { From 141c1880b375073d1d07881db938544a9aec9e7b Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:46:59 +0100 Subject: [PATCH 06/12] javadocs --- modules/javafx.graphics/src/main/java/javafx/stage/Stage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 85b3012d71c..b2358df07a6 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -357,6 +357,7 @@ public final void show(double anchorX, double anchorY, AnchorPoint anchor) { * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges * when clamping is performed * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} + * @throws IllegalArgumentException if {@code screenPadding} is negative * @since 26 */ public final void show(double anchorX, double anchorY, AnchorPoint anchor, @@ -575,6 +576,7 @@ public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges * when clamping is performed * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} + * @throws IllegalArgumentException if {@code screenPadding} is negative * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread * @throws IllegalStateException if this method is called during animation or layout processing * @throws IllegalStateException if this call would exceed the maximum number of nested event loops From d0a56eccd51d107edf2a998ab661a5c3e2f93d29 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 27 Nov 2025 23:31:46 +0100 Subject: [PATCH 07/12] refactor everything --- .../sun/javafx/stage/WindowBoundsUtil.java | 295 ++++++++++ .../main/java/javafx/stage/AnchorPolicy.java | 94 +++ .../main/java/javafx/stage/ClampPolicy.java | 69 --- .../main/java/javafx/stage/PopupWindow.java | 64 ++- .../src/main/java/javafx/stage/Stage.java | 337 +++-------- .../src/main/java/javafx/stage/Window.java | 25 +- .../java/test/javafx/stage/StageTest.java | 533 +++++++++++++----- 7 files changed, 932 insertions(+), 485 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java delete mode 100644 modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java new file mode 100644 index 00000000000..dbfd98dbb3b --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.stage; + +import com.sun.javafx.util.Utils; +import javafx.geometry.AnchorPoint; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.geometry.Rectangle2D; +import javafx.stage.AnchorPolicy; +import javafx.stage.Screen; +import javafx.stage.Window; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class WindowBoundsUtil { + + private WindowBoundsUtil() {} + + /** + * Creates a relocation operation that positions a {@link Window} at the requested screen coordinates + * using an {@link AnchorPoint}, {@link AnchorPolicy}, and per-edge screen constraints. + *

+ * Screen edge constraints are specified by {@code screenPadding}: + * values {@code >= 0} enable a constraint for the corresponding edge (minimum distance to keep), + * values {@code < 0} disable the constraint for that edge. Enabled constraints reduce the usable area + * for placement by the given insets. + */ + public static Consumer newDeferredRelocation(double screenX, double screenY, + AnchorPoint anchor, AnchorPolicy anchorPolicy, + Insets screenPadding) { + Objects.requireNonNull(anchor, "anchor cannot be null"); + Objects.requireNonNull(anchorPolicy, "anchorPolicy cannot be null"); + Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); + + return window -> { + Screen currentScreen = Utils.getScreenForPoint(screenX, screenY); + Rectangle2D screenBounds = Utils.hasFullScreenStage(currentScreen) + ? currentScreen.getBounds() + : currentScreen.getVisualBounds(); + + Point2D location = computeAdjustedLocation( + screenX, screenY, + window.getWidth(), window.getHeight(), + anchor, anchorPolicy, + screenBounds, screenPadding); + + window.setX(location.getX()); + window.setY(location.getY()); + }; + } + + /** + * Computes the adjusted top-left location of a window for a requested anchor position on screen. + *

+ * The requested screen coordinates {@code (screenX, screenY)} are interpreted as the desired location + * of {@code anchor} on the window. The raw (unadjusted) window position is derived from the anchor and + * the given {@code width}/{@code height}. If that raw position violates any enabled constraints, the + * method considers alternative anchors depending on {@code policy} (for example, horizontally and/or + * vertically flipped anchors) and chooses the alternative that yields the smallest adjustment after + * constraints are applied. + *

+ * Screen edge constraints are specified by {@code screenPadding}: + * values {@code >= 0} enable a constraint for the corresponding edge (minimum distance to keep), + * values {@code < 0} disable the constraint for that edge. Enabled constraints reduce the usable area + * for placement by the given insets. + */ + public static Point2D computeAdjustedLocation(double screenX, double screenY, + double width, double height, + AnchorPoint anchor, AnchorPolicy policy, + Rectangle2D screenBounds, Insets screenPadding) { + Constraints constraints = computeConstraints(screenBounds, width, height, screenPadding); + Position preferredRaw = getRawForAnchor(screenX, screenY, anchor, width, height); + boolean validH = isHorizontalValid(preferredRaw, constraints); + boolean validV = isVerticalValid(preferredRaw, constraints); + if (validH && validV) { + return new Point2D(preferredRaw.x, preferredRaw.y); + } + + List alternatives = computeAlternatives(anchor, policy, validH, validV, width, height); + Point2D bestAdjusted = applyConstraints(preferredRaw, constraints); + double bestCost = getAdjustmentCost(preferredRaw, bestAdjusted); + + for (AnchorPoint alternative : alternatives) { + Position raw = getRawForAnchor(screenX, screenY, alternative, width, height); + Point2D adjusted = applyConstraints(raw, constraints); + double cost = getAdjustmentCost(raw, adjusted); + + if (cost < bestCost) { + bestCost = cost; + bestAdjusted = adjusted; + } + } + + return bestAdjusted; + } + + /** + * Computes effective constraints from screen bounds, window size, and edge insets. + *

+ * For each inset value: + *

+ * Enabled constraints shrink the usable region by the given amounts. The computed {@code maxX} + * and {@code maxY} incorporate the window size (i.e., they are the maximum allowed top-left + * coordinates that still keep the window within the constrained region). + */ + private static Constraints computeConstraints(Rectangle2D screenBounds, + double width, double height, + Insets screenPadding) { + boolean hasMinX = screenPadding.getLeft() >= 0; + boolean hasMaxX = screenPadding.getRight() >= 0; + boolean hasMinY = screenPadding.getTop() >= 0; + boolean hasMaxY = screenPadding.getBottom() >= 0; + + double minX = screenBounds.getMinX() + (hasMinX ? screenPadding.getLeft() : 0); + double maxX = screenBounds.getMaxX() - (hasMaxX ? screenPadding.getRight() : 0) - width; + double minY = screenBounds.getMinY() + (hasMinY ? screenPadding.getTop() : 0); + double maxY = screenBounds.getMaxY() - (hasMaxY ? screenPadding.getBottom() : 0) - height; + + return new Constraints(hasMinX, hasMaxX, hasMinY, hasMaxY, minX, maxX, minY, maxY); + } + + /** + * Computes the raw (unadjusted) top-left position for the given anchor. + *

+ * The result is the position at which the window would be located if no edge constraints were applied. + */ + private static Position getRawForAnchor(double screenX, double screenY, AnchorPoint anchor, + double width, double height) { + double x, y, relX, relY; + + if (anchor.isProportional()) { + x = width * anchor.getX(); + y = height * anchor.getY(); + relX = anchor.getX(); + relY = anchor.getY(); + } else { + x = anchor.getX(); + y = anchor.getY(); + relX = width != 0 ? anchor.getX() / width : 0; + relY = height != 0 ? anchor.getY() / height : 0; + } + + return new Position(screenX - x, screenY - y, relX, relY); + } + + /** + * Computes the list of alternative candidate anchors to consider, based on the requested policy + * and which constraint the preferred placement violates. + *

+ * Candidates are ordered from most preferred to least preferred for the given policy. + */ + private static List computeAlternatives(AnchorPoint preferred, AnchorPolicy policy, + boolean validH, boolean validV, + double width, double height) { + return switch (policy) { + case FIXED -> List.of(); + + case FLIP_HORIZONTAL -> validH + ? List.of() + : List.of(flipAnchor(preferred, width, height, true, false)); + + case FLIP_VERTICAL -> validV + ? List.of() + : List.of(flipAnchor(preferred, width, height, false, true)); + + case AUTO -> { + if (!validH && !validV) { + // Try diagonal flip first, then horizontal flip, then vertical flip + yield List.of( + flipAnchor(preferred, width, height, true, true), + flipAnchor(preferred, width, height, true, false), + flipAnchor(preferred, width, height, false, true)); + } else if (!validH) { + yield List.of(flipAnchor(preferred, width, height, true, false)); + } else if (!validV) { + yield List.of(flipAnchor(preferred, width, height, false, true)); + } else{ + yield List.of(); + } + } + }; + } + + /** + * Applies enabled edge constraints to a raw position. + *

+ * Constraints may be disabled per edge (via negative inset values). When both edges for an axis + * are enabled, the position is constrained to the resulting interval. When only one edge is enabled, + * a one-sided minimum or maximum constraint is applied. If the constrained interval is too small to + * fit the window, a side is chosen based on the relative anchor location. + */ + private static Point2D applyConstraints(Position raw, Constraints c) { + double x = raw.x; + double y = raw.y; + + if (c.hasMinX && c.hasMaxX) { + if (c.maxX >= c.minX) { + x = Utils.clamp(c.minX, x, c.maxX); + } else { + // Constrained space too small: choose a side based on anchor + x = raw.relX > 0.5 ? c.maxX : c.minX; + } + } else if (c.hasMinX) { + x = Math.max(x, c.minX); + } else if (c.hasMaxX) { + x = Math.min(x, c.maxX); + } + + if (c.hasMinY && c.hasMaxY) { + if (c.maxY >= c.minY) { + y = Utils.clamp(c.minY, y, c.maxY); + } else { + // Constrained space too small: choose a side based on anchor + y = raw.relY > 0.5 ? c.maxY : c.minY; + } + } else if (c.hasMinY) { + y = Math.max(y, c.minY); + } else if (c.hasMaxY) { + y = Math.min(y, c.maxY); + } + + return new Point2D(x, y); + } + + /** + * Computes a scalar "adjustment cost" used to select between candidate anchors. + *

+ * The current implementation uses Manhattan distance (|dx| + |dy|) between the raw and adjusted positions. + * Lower values indicate that fewer or smaller constraint adjustments were required. + */ + private static double getAdjustmentCost(Position raw, Point2D adjusted) { + return Math.abs(adjusted.getX() - raw.x) + Math.abs(adjusted.getY() - raw.y); + } + + private static boolean isHorizontalValid(Position raw, Constraints c) { + return !(c.hasMinX && raw.x < c.minX) && !(c.hasMaxX && raw.x > c.maxX); + } + + private static boolean isVerticalValid(Position raw, Constraints c) { + return !(c.hasMinY && raw.y < c.minY) && !(c.hasMaxY && raw.y > c.maxY); + } + + private static AnchorPoint flipAnchor(AnchorPoint anchor, + double width, double height, + boolean flipH, boolean flipV) { + if (anchor.isProportional()) { + double x = anchor.getX(); + double y = anchor.getY(); + double nx = flipH ? (1.0 - x) : x; + double ny = flipV ? (1.0 - y) : y; + return AnchorPoint.proportional(nx, ny); + } else { + double x = anchor.getX(); + double y = anchor.getY(); + double nx = flipH ? (width - x) : x; + double ny = flipV ? (height - y) : y; + return AnchorPoint.absolute(nx, ny); + } + } + + private record Constraints(boolean hasMinX, boolean hasMaxX, + boolean hasMinY, boolean hasMaxY, + double minX, double maxX, + double minY, double maxY) {} + + private record Position(double x, double y, double relX, double relY) {} +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java new file mode 100644 index 00000000000..0a9ec0c2ad2 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.stage; + +import javafx.geometry.AnchorPoint; +import javafx.geometry.Insets; + +/** + * Specifies how a repositioning operation may adjust an anchor point when the preferred placement + * would violate the screen bounds constraints. + *

+ * The anchor passed to {@link Stage#relocate(double, double, AnchorPoint, AnchorPolicy, Insets)} or specified + * by {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point on the + * window that should coincide with the requested screen coordinates. When the preferred anchor would place + * the window outside the allowed screen area (as defined by the screen bounds and any configured insets), + * an {@code AnchorPolicy} can be used to select an alternative anchor before applying any final position + * adjustment. + * + * @since 26 + */ +public enum AnchorPolicy { + + /** + * Always use the preferred anchor and never select an alternative anchor. + *

+ * If the preferred placement violates the allowed screen area, the window position is adjusted + * for the window to fall within the allowed screen area. If this is not possible, the window is + * biased towards the edge that is closer to the anchor. + */ + FIXED, + + /** + * If the preferred placement violates horizontal constraints, attempt a horizontally flipped anchor. + *

+ * A horizontal flip mirrors the anchor across the vertical center line of the window + * (for example, {@code TOP_LEFT} becomes {@code TOP_RIGHT}). + *

+ * If the horizontally flipped anchor does not improve the placement, the original anchor is used + * and the final position is adjusted for the window to fall within the allowed screen area. + * If this is not possible, the window is biased towards the edge that is closer to the anchor. + */ + FLIP_HORIZONTAL, + + /** + * If the preferred placement violates vertical constraints, attempt a vertically flipped anchor. + *

+ * A vertical flip mirrors the anchor across the horizontal center line of the window + * (for example, {@code TOP_LEFT} becomes {@code BOTTOM_LEFT}). + *

+ * If the vertically flipped anchor does not improve the placement, the original anchor is used + * and the final position is adjusted for the window to fall within the allowed screen area. + * If this is not possible, the window is biased towards the edge that is closer to the anchor. + */ + FLIP_VERTICAL, + + /** + * Automatically chooses an alternative anchor based on which constraints are violated. + *

+ * This policy selects the "most natural" flip for the current situation: + *

+ * If no alternative anchor yields a better placement, the original anchor is used and the final + * position is adjusted for the window to fall within the allowed screen area. + * If this is not possible, the window is biased towards the edge that is closer to the anchor. + */ + AUTO +} \ No newline at end of file diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java deleted file mode 100644 index 7a921642d18..00000000000 --- a/modules/javafx.graphics/src/main/java/javafx/stage/ClampPolicy.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package javafx.stage; - -/** - * Specifies how a {@link Stage} should be clamped to the screen bounds. - *

- * Clamping adjusts the computed window position so that the window is shown within the screen bounds. - * Clamping can be applied independently on the horizontal axis, the vertical axis, both axes, or not at all. - * - * @since 26 - */ -public enum ClampPolicy { - - /** - * Do not clamp the computed position. - *

- * The window is placed exactly as specified by the requested screen coordinates, even if this - * causes parts of the window to extend beyond the bounds of the screen. - */ - NONE, - - /** - * Clamp the computed position horizontally only. - *

- * The {@code x} coordinate of the window is adjusted as needed to keep the window within the screen - * bounds, while the {@code y} coordinate is left unchanged. - */ - HORIZONTAL, - - /** - * Clamp the computed position vertically only. - *

- * The {@code y} coordinate of the window is adjusted as needed to keep the window within the screen - * bounds, while the {@code x} coordinate is left unchanged. - */ - VERTICAL, - - /** - * Clamp the computed position both horizontally and vertically. - *

- * Both the {@code x} and {@code y} coordinates of the window are adjusted as needed to keep the - * window within the screen bounds. - */ - BOTH -} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java index a7b68f0fa54..b47978fc294 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java @@ -44,8 +44,11 @@ import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.geometry.AnchorPoint; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.Node; @@ -59,6 +62,7 @@ import com.sun.javafx.scene.SceneHelper; import com.sun.javafx.stage.FocusUngrabEvent; import com.sun.javafx.stage.PopupWindowPeerListener; +import com.sun.javafx.stage.WindowBoundsUtil; import com.sun.javafx.stage.WindowCloseRequestHandler; import com.sun.javafx.stage.WindowEventDispatcher; import com.sun.javafx.tk.Toolkit; @@ -631,6 +635,32 @@ public final ReadOnlyDoubleProperty anchorYProperty() { return anchorY.getReadOnlyProperty(); } + /** + * Controls whether an alternative anchor location may be used when the preferred + * {@link #anchorLocationProperty() anchorLocation} would place the popup window outside the screen bounds. + * Depending on the policy, the preferred anchor location may be mirrored to the other side of the window + * horizontally or vertically, or an anchor location may be selected automatically. + *

+ * If no alternative anchor location yields a better placement, the specified {@code anchorLocation} is used. + * + * @defaultValue {@link AnchorPolicy#FIXED} + * @since 26 + */ + private final ObjectProperty anchorPolicy = + new SimpleObjectProperty<>(this, "anchorPolicy", AnchorPolicy.FIXED); + + public final ObjectProperty anchorPolicyProperty() { + return anchorPolicy; + } + + public final AnchorPolicy getAnchorPolicy() { + return anchorPolicy.get(); + } + + public final void setAnchorPolicy(AnchorPolicy value) { + anchorPolicy.set(value); + } + /** * Specifies the popup anchor point which is used in popup positioning. The * point can be set to a corner of the popup window or a corner of its @@ -783,34 +813,14 @@ private void updateWindow(final double newAnchorX, ? currentScreen.getBounds() : currentScreen.getVisualBounds(); - if (anchorXCoef <= 0.5) { - // left side of the popup is more important, try to keep it - // visible if the popup width is larger than screen width - anchorScrMinX = Math.min(anchorScrMinX, - screenBounds.getMaxX() - - anchorBounds.getWidth()); - anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX()); - } else { - // right side of the popup is more important - anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX()); - anchorScrMinX = Math.min(anchorScrMinX, - screenBounds.getMaxX() - - anchorBounds.getWidth()); - } + Point2D location = WindowBoundsUtil.computeAdjustedLocation( + newAnchorX, newAnchorY, + anchorBounds.getWidth(), anchorBounds.getHeight(), + AnchorPoint.proportional(anchorXCoef, anchorYCoef), + getAnchorPolicy(), screenBounds, Insets.EMPTY); - if (anchorYCoef <= 0.5) { - // top side of the popup is more important - anchorScrMinY = Math.min(anchorScrMinY, - screenBounds.getMaxY() - - anchorBounds.getHeight()); - anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY()); - } else { - // bottom side of the popup is more important - anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY()); - anchorScrMinY = Math.min(anchorScrMinY, - screenBounds.getMaxY() - - anchorBounds.getHeight()); - } + anchorScrMinX = location.getX(); + anchorScrMinY = location.getY(); } final double windowScrMinX = diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index b2358df07a6..65e2b6aa0a6 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,7 +27,7 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; +import java.util.function.Consumer; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -40,7 +40,6 @@ import javafx.geometry.AnchorPoint; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; -import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.input.KeyCombination; @@ -54,9 +53,9 @@ import com.sun.javafx.stage.HeaderButtonMetrics; import com.sun.javafx.stage.StageHelper; import com.sun.javafx.stage.StagePeerListener; +import com.sun.javafx.stage.WindowBoundsUtil; import com.sun.javafx.tk.TKStage; import com.sun.javafx.tk.Toolkit; -import com.sun.javafx.util.Utils; import javafx.beans.NamedArg; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; @@ -302,76 +301,6 @@ public Stage(@NamedArg(value="style", defaultValue="DECORATED") StageStyle style super.show(); } - /** - * Shows this stage at the specified location and adjusts the position as needed to keep the stage - * visible on screen. If the stage is already showing, it is moved to the computed position instead. - *

- * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the - * stage size, and is then adjusted to keep the stage within the screen bounds. - *

- * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will - * reflect the adjusted position. - *

- * Calling this method is equivalent to calling - * {@code show(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY)}. - * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen - * @throws NullPointerException if {@code anchor} is {@code null} - * @since 26 - */ - public final void show(double anchorX, double anchorY, AnchorPoint anchor) { - show(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY); - } - - /** - * Shows this stage at the specified location using the given anchor and clamping options. - * If the stage is already showing, it is moved to the computed position instead. - *

- * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the - * stage size, and is then adjusted according to {@code clampPolicy}. - *

- * The {@code clampPolicy} controls which axes are clamped to the screen bounds: - *

- * If clamping is performed, {@code screenPadding} specifies additional space to maintain between the - * stage edges and the screen edges. The padding is applied per edge (top/right/bottom/left) and - * effectively shrinks the usable screen area for clamping by the given insets. For example, a left - * padding of {@code 10} ensures that, after clamping, the stage will not be placed closer than - * 10 pixels to the left screen edge (and similarly for the other edges). - *

- * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will - * reflect the adjusted position. - * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen - * @param clampPolicy controls whether clamping is performed horizontally, vertically, both, or not at all - * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges - * when clamping is performed - * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} - * @throws IllegalArgumentException if {@code screenPadding} is negative - * @since 26 - */ - public final void show(double anchorX, double anchorY, AnchorPoint anchor, - ClampPolicy clampPolicy, Insets screenPadding) { - var positionRequest = new PositionRequest(anchorX, anchorY, anchor, clampPolicy, screenPadding); - - if (isShowing()) { - positionRequest.apply(this); - } else { - this.positionRequest = positionRequest; - super.show(); - } - } - private boolean primary = false; //------------------------------------------------------------------ @@ -506,94 +435,7 @@ private boolean isImportant() { * @since JavaFX 2.2 */ public void showAndWait() { - verifyCanShowAndWait(); - super.show(); - inNestedEventLoop = true; - Toolkit.getToolkit().enterNestedEventLoop(this); - } - /** - * Shows this stage at the specified location and adjusts the position as needed to keep the stage - * visible on screen. This method blocks until the stage is hidden before returning to the caller. - * It also temporarily blocks processing of the current event and starts a nested event loop to - * handle other events. This method must be called on the JavaFX application thread. - *

- * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the - * stage size, and is then adjusted to keep the stage within the screen bounds. - *

- * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will - * reflect the adjusted position. - *

- * Calling this method is equivalent to calling - * {@code showAndWait(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY)}. - * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen - * @throws NullPointerException if {@code anchor} is {@code null} - * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread - * @throws IllegalStateException if this method is called during animation or layout processing - * @throws IllegalStateException if this call would exceed the maximum number of nested event loops - * @throws IllegalStateException if this method is called on the primary stage - * @throws IllegalStateException if this stage is already showing - * @since 26 - */ - public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor) { - showAndWait(anchorX, anchorY, anchor, ClampPolicy.BOTH, Insets.EMPTY); - } - - /** - * Shows this stage at the specified location using the given anchor and clamping options. - * This method blocks until the stage is hidden before returning to the caller. - * It also temporarily blocks processing of the current event and starts a nested event loop to - * handle other events. This method must be called on the JavaFX application thread. - *

- * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is computed from the anchor and the - * stage size, and is then adjusted according to {@code clampPolicy}. - *

- * The {@code clampPolicy} controls which axes are clamped to the screen bounds: - *

- * If clamping is performed, {@code screenPadding} specifies additional space to maintain between the - * stage edges and the screen edges. The padding is applied per edge (top/right/bottom/left) and - * effectively shrinks the usable screen area for clamping by the given insets. For example, a left - * padding of {@code 10} ensures that, after clamping, the stage will not be placed closer than - * 10 pixels to the left screen edge (and similarly for the other edges). - *

- * After the stage is shown, its {@link #xProperty() X} and {@link #yProperty() Y} properties will - * reflect the adjusted position. - * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen - * @param clampPolicy controls whether clamping is performed horizontally, vertically, both, or not at all - * @param screenPadding the minimum padding to maintain between the stage edges and the screen edges - * when clamping is performed - * @throws NullPointerException if {@code anchor}, {@code clampPolicy}, or {@code screenPadding} is {@code null} - * @throws IllegalArgumentException if {@code screenPadding} is negative - * @throws IllegalStateException if this method is called on a thread other than the JavaFX application thread - * @throws IllegalStateException if this method is called during animation or layout processing - * @throws IllegalStateException if this call would exceed the maximum number of nested event loops - * @throws IllegalStateException if this method is called on the primary stage - * @throws IllegalStateException if this stage is already showing - * @since 26 - */ - public final void showAndWait(double anchorX, double anchorY, AnchorPoint anchor, - ClampPolicy clampPolicy, Insets screenPadding) { - verifyCanShowAndWait(); - positionRequest = new PositionRequest(anchorX, anchorY, anchor, clampPolicy, screenPadding); - super.show(); - inNestedEventLoop = true; - Toolkit.getToolkit().enterNestedEventLoop(this); - } - - private void verifyCanShowAndWait() { Toolkit.getToolkit().checkFxUserThread(); if (isPrimary()) { @@ -612,6 +454,10 @@ private void verifyCanShowAndWait() { // method is called from an event handler that is listening to a // WindowEvent.WINDOW_HIDING event. assert !inNestedEventLoop; + + show(); + inNestedEventLoop = true; + Toolkit.getToolkit().enterNestedEventLoop(this); } private StageStyle style; // default is set in constructor @@ -1371,6 +1217,88 @@ public void toBack() { } } + /** + * Moves this stage to the specified screen location using the given anchor. + *

+ * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is then + * adjusted to keep the stage within the screen bounds. + *

+ * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. + * If called before the stage is shown, then + *

    + *
  1. any previous call to {@link #centerOnScreen()} is canceled, + *
  2. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated + * immediately; instead, they are updated after the stage is shown. + *
+ * Calling this method is equivalent to calling + * {@code relocate(anchorX, anchorY, anchor, AnchorPolicy.FIXED, Insets.EMPTY)}. + * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @throws NullPointerException if {@code anchor} is {@code null} + * @since 26 + */ + public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { + relocate(anchorX, anchorY, anchor, AnchorPolicy.FIXED, Insets.EMPTY); + } + + /** + * Moves this stage to the specified screen location using the given anchor, anchor-selection policy, + * and screen edge constraints. + *

+ * The {@code anchor} identifies a point on the stage that should coincide with the requested screen + * coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is then + * optionally adjusted to keep the stage within screen edge constraints. + *

+ * The {@code anchorPolicy} controls whether an alternative anchor may be used when the preferred anchor + * would violate screen edge constraints. Depending on the policy, the preferred anchor location may be + * mirrored to the other side of the window horizontally or vertically, or an anchor might be selected + * automatically. If no alternative anchor yields a better placement, the specified {@code anchor} is used. + *

+ * The {@code screenPadding} parameter defines per-edge constraints against the current screen bounds. + * Each inset value specifies the minimum distance to maintain between the stage edge and the corresponding + * screen edge. A value {@code >= 0} enables the corresponding edge constraint; a negative value disables + * the constraint for that edge. Enabled constraints effectively shrink the usable screen area by the + * given insets. For example, a left inset of {@code 10} ensures the stage will not be placed closer than + * 10 pixels to the left screen edge. + *

+ * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. + * If called before the stage is shown, then + *

    + *
  1. any previous call to {@link #centerOnScreen()} is canceled, + *
  2. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated + * immediately; instead, they are updated after the stage is shown. + *
+ * + * @param anchorX the requested horizontal location of the anchor point on the screen + * @param anchorY the requested vertical location of the anchor point on the screen + * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @param anchorPolicy controls alternative anchor selection if the preferred placement violates + * enabled screen constraints + * @param screenPadding per-edge minimum distance constraints relative to the screen edges; + * values {@code < 0} disable the constraint for the respective edge + * @throws NullPointerException if {@code anchor}, {@code anchorPolicy}, or {@code screenPadding} is {@code null} + * @since 26 + */ + public final void relocate(double anchorX, double anchorY, AnchorPoint anchor, + AnchorPolicy anchorPolicy, Insets screenPadding) { + var request = WindowBoundsUtil.newDeferredRelocation(anchorX, anchorY, anchor, anchorPolicy, screenPadding); + + if (isShowing()) { + request.accept(this); + } else { + this.relocationRequest = request; + } + } + + @Override + public void centerOnScreen() { + relocationRequest = null; // cancel previous relocation request + super.centerOnScreen(); + } + /** * Closes this {@code Stage}. * This call is equivalent to {@code hide()}. @@ -1475,92 +1403,9 @@ private void setPrefHeaderButtonHeight(double height) { } @Override - final void fixBounds() { - if (positionRequest != null) { - positionRequest.apply(this); - positionRequest = null; - } + final Consumer getBoundsConfigurator() { + return relocationRequest; } - private PositionRequest positionRequest; - - private record PositionRequest(double screenX, double screenY, AnchorPoint anchor, - ClampPolicy clampPolicy, Insets screenPadding) { - - PositionRequest { - Objects.requireNonNull(anchor, "anchor cannot be null"); - Objects.requireNonNull(clampPolicy, "clampPolicy cannot be null"); - Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); - - if (screenPadding.getTop() < 0 - || screenPadding.getRight() < 0 - || screenPadding.getBottom() < 0 - || screenPadding.getLeft() < 0) { - throw new IllegalArgumentException("screenPadding cannot be negative"); - } - } - - void apply(Stage stage) { - Screen currentScreen = Utils.getScreenForPoint(screenX, screenY); - Rectangle2D screenBounds = Utils.hasFullScreenStage(currentScreen) - ? currentScreen.getBounds() - : currentScreen.getVisualBounds(); - - double width = stage.getWidth(); - double height = stage.getHeight(); - double anchorX, anchorY; - double anchorRelX, anchorRelY; - - if (anchor.isProportional()) { - anchorX = width * anchor.getX(); - anchorY = height * anchor.getY(); - anchorRelX = anchor.getX(); - anchorRelY = anchor.getY(); - } else { - anchorX = anchor.getX(); - anchorY = anchor.getY(); - anchorRelX = width != 0 ? anchor.getX() / width : 0; - anchorRelY = height != 0 ? anchor.getY() / height : 0; - } - - // Raw (unclamped) top-left position derived from the requested screen point + anchor - double rawX = screenX - anchorX; - double rawY = screenY - anchorY; - - // Start with raw coordinates; clamp per policy below - double x = rawX; - double y = rawY; - - // Only compute clamp ranges for axes that are being clamped - boolean clampH = clampPolicy == ClampPolicy.BOTH || clampPolicy == ClampPolicy.HORIZONTAL; - boolean clampV = clampPolicy == ClampPolicy.BOTH || clampPolicy == ClampPolicy.VERTICAL; - - if (clampH) { - double minX = screenBounds.getMinX() + screenPadding.getLeft(); - double maxX = screenBounds.getMaxX() - screenPadding.getRight() - width; - - if (maxX >= minX) { - x = Utils.clamp(minX, rawX, maxX); - } else { - // Window (plus padding) doesn't fit horizontally: pick a side based on anchor - x = anchorRelX > 0.5 ? maxX : minX; - } - } - - if (clampV) { - double minY = screenBounds.getMinY() + screenPadding.getTop(); - double maxY = screenBounds.getMaxY() - screenPadding.getBottom() - height; - - if (maxY >= minY) { - y = Utils.clamp(minY, rawY, maxY); - } else { - // Window (plus padding) doesn't fit vertically: pick a side based on anchor - y = anchorRelY > 0.5 ? maxY : minY; - } - } - - stage.setX(x); - stage.setY(y); - } - } + private Consumer relocationRequest; } diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java index a947e69e241..07327cec085 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java @@ -26,6 +26,7 @@ package javafx.stage; import java.util.HashMap; +import java.util.function.Consumer; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -1060,7 +1061,10 @@ public final ObjectProperty> onHiddenProperty() { } else { windows.remove(Window.this); } + Toolkit tk = Toolkit.getToolkit(); + Consumer boundsConfigurator = getBoundsConfigurator(); + if (peer != null) { if (newVisible) { if (peerListener == null) { @@ -1117,8 +1121,10 @@ public final ObjectProperty> onHiddenProperty() { 0, 0); } - // Give subclasses a chance to adjust the window bounds - fixBounds(); + // If a derived class has provided us a bounds configurator, now is the time to apply it. + if (boundsConfigurator != null) { + boundsConfigurator.accept(Window.this); + } // set peer bounds before the window is shown applyBounds(); @@ -1159,6 +1165,11 @@ public final ObjectProperty> onHiddenProperty() { // might have changed (e.g. due to setResizable(false)). Reapply the // sizeToScene() request if needed to account for the new insets. sizeToScene(); + + // If the window size has changed, we need to run the bounds configurator again. + if (boundsConfigurator != null) { + boundsConfigurator.accept(Window.this); + } } // Reset the flag unconditionally upon visibility changes @@ -1366,12 +1377,18 @@ private void focusChanged(final boolean newIsFocused) { } } - void fixBounds() {} - final void applyBounds() { peerBoundsConfigurator.apply(); } + /** + * Allows subclasses to specify an algorithm that adjusts the window bounds + * just before the window is shown. + */ + Consumer getBoundsConfigurator() { + return null; + } + Window getWindowOwner() { return null; } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 161fca102d1..65fbf53c411 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -32,7 +32,7 @@ import javafx.scene.image.Image; import javafx.scene.Group; import javafx.scene.Scene; -import javafx.stage.ClampPolicy; +import javafx.stage.AnchorPolicy; import javafx.stage.Stage; import test.com.sun.javafx.pgstub.StubStage; import test.com.sun.javafx.pgstub.StubToolkit; @@ -45,12 +45,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class StageTest { @@ -63,6 +60,8 @@ public class StageTest { @BeforeEach public void setUp() { toolkit = (StubToolkit) Toolkit.getToolkit(); + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + s = new Stage(); s.setOnShown(_ -> { peer = (StubStage) WindowHelper.getPeer(s); @@ -73,6 +72,7 @@ public void setUp() { @AfterEach public void tearDown() { s.hide(); + toolkit.resetScreens(); } private void pulse() { @@ -552,174 +552,429 @@ public void testAddAndSetNullIcon() { } } - /** - * Tests that a stage that is shown with an anchor and placed such that it extends slightly beyond - * the edges of the screen is repositioned so that it fits within the screen, taking into account - * padding around the screen. - */ - @ParameterizedTest(name = "Clamps to {0} edges with {1} anchor") - @MethodSource("showWithAnchorClampsWindowToScreenEdges_arguments") - public void showAndClampToScreenEdgesWithPadding( - @SuppressWarnings("unused") String edge, - @SuppressWarnings("unused") String anchorName, - AnchorPoint anchor, - ClampPolicy clampPolicy, - Insets screenPadding, - double screenW, double screenH, - double stageW, double stageH, - double requestX, double requestY) { - toolkit.setScreens( - new ScreenConfiguration( - 0, 0, (int)screenW, (int)screenH, - 0, 0, (int)screenW, (int)screenH, - 96)); + @Test + public void relocateNullArgumentsThrowNPE() { + s.show(); + pulse(); + assertNotNull(peer); + assertThrows(NullPointerException.class, () -> s.relocate(0, 0, null, AnchorPolicy.FIXED, Insets.EMPTY)); + assertThrows(NullPointerException.class, () -> s.relocate(0, 0, AnchorPoint.TOP_LEFT, null, Insets.EMPTY)); + assertThrows(NullPointerException.class, () -> s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, null)); + } - try { - s.setWidth(stageW); - s.setHeight(stageH); - s.show(requestX, requestY, anchor, clampPolicy, screenPadding); - pulse(); + @Test + public void relocateBeforeShowPositionsStageOnShow() { + s.setWidth(300); + s.setHeight(200); + s.relocate(100, 120, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + pulse(); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), screenPadding); - } finally { - toolkit.resetScreens(); - } + assertEquals(100, peer.x, 0.0001); + assertEquals(120, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } - private static Stream showWithAnchorClampsWindowToScreenEdges_arguments() { - final double screenW = 800; - final double screenH = 600; - final double stageW = 200; - final double stageH = 200; - final double overshoot = 10; // push past the edge to force clamping + @Test + public void relocateAfterShowMovesStageImmediately() { + s.setWidth(300); + s.setHeight(200); + s.show(); + pulse(); - Stream.Builder b = Stream.builder(); + s.relocate(200, 220, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + pulse(); - // no screen padding - b.add(Arguments.of("bottom and right", "TOP_LEFT", AnchorPoint.TOP_LEFT, - ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, - stageW, stageH, screenW - stageW + overshoot, screenH - stageH + overshoot)); - b.add(Arguments.of("bottom and left", "TOP_RIGHT", AnchorPoint.TOP_RIGHT, - ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, - stageW, stageH, stageW - overshoot, screenH - stageH + overshoot)); - b.add(Arguments.of("top and right", "BOTTOM_LEFT", AnchorPoint.BOTTOM_LEFT, - ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, - stageW, stageH, screenW - stageW + overshoot, stageH - overshoot)); - b.add(Arguments.of("top and left", "BOTTOM_RIGHT", AnchorPoint.BOTTOM_RIGHT, - ClampPolicy.BOTH, Insets.EMPTY, screenW, screenH, - stageW, stageH, stageW - overshoot, stageH - overshoot)); - - // with screen padding - b.add(Arguments.of("bottom and right", "TOP_LEFT", AnchorPoint.TOP_LEFT, - ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, - stageW, stageH, screenW - stageW + overshoot, screenH - stageH + overshoot)); - b.add(Arguments.of("bottom and left", "TOP_RIGHT", AnchorPoint.TOP_RIGHT, - ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, - stageW, stageH, stageW - overshoot, screenH - stageH + overshoot)); - b.add(Arguments.of("top and right", "BOTTOM_LEFT", AnchorPoint.BOTTOM_LEFT, - ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, - stageW, stageH, screenW - stageW + overshoot, stageH - overshoot)); - b.add(Arguments.of("top and left", "BOTTOM_RIGHT", AnchorPoint.BOTTOM_RIGHT, - ClampPolicy.BOTH, new Insets(10, 20, 30, 40), screenW, screenH, - stageW, stageH, stageW - overshoot, stageH - overshoot)); - return b.build(); + assertEquals(200, peer.x, 0.0001); + assertEquals(220, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } - /** - * Tests that the {@link ClampPolicy} is taken into account when clamping to screen edges. - */ - @ParameterizedTest - @CsvSource({ - "NONE, 790, 590", - "HORIZONTAL, 600, 590", - "VERTICAL, 790, 500", - "BOTH, 600, 500", - }) - public void showWithClampPolicy(ClampPolicy clampPolicy, double expectX, double expectY) { - toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + @Test + public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { + s.setWidth(200); + s.setHeight(200); + s.centerOnScreen(); - try { - s.setWidth(200); - s.setHeight(100); - s.show(790, 590, AnchorPoint.absolute(0, 0), clampPolicy, Insets.EMPTY); - pulse(); + // If centerOnScreen were honored, we'd expect (300, 200) on 800x600. + // relocate should override/cancel it. + s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + pulse(); - assertTrue(s.isShowing()); - assertEquals(expectX, peer.x, 0.0001); - assertEquals(expectY, peer.y, 0.0001); + assertEquals(0, peer.x, 0.0001); + assertEquals(0, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } - if (clampPolicy != ClampPolicy.BOTH) { - assertNotWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); - } else { - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); - } - } finally { - toolkit.resetScreens(); - } + @Test + public void relocateHonorsPaddingForEnabledEdges() { + s.setWidth(200); + s.setHeight(200); + + var padding = new Insets(10, 20, 30, 40); // top, right, bottom, left + + // Ask to place the TOP_LEFT anchor beyond the bottom-right safe area to force adjustment + s.relocate(800, 600, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.show(); + pulse(); + + // Allowed top-left: x <= 800 - 20 - 200 = 580, y <= 600 - 30 - 200 = 370 + assertEquals(580, peer.x, 0.0001); + assertEquals(370, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), padding); } @Test - public void showWithAnchorMovesStageWhenAlreadyShowing() { - toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96)); + public void relocateNegativeInsetsDisableConstraintsPerEdge() { + s.setWidth(300); + s.setHeight(200); - try { - s.setWidth(200); - s.setHeight(100); + // Disable right and bottom constraints (negative), keep left/top enabled at 0. + var padding = new Insets(0, -1, -1, 0); + s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.show(); + pulse(); - s.show(10, 10, AnchorPoint.absolute(0, 0)); - pulse(); - assertTrue(s.isShowing()); - assertEquals(10, peer.x, 0.0001); - assertEquals(10, peer.y, 0.0001); + assertEquals(790, peer.x, 0.0001); + assertEquals(590, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().getFirst(), padding); + } - // Calling show again should reposition - s.show(120, 140, AnchorPoint.absolute(0, 0)); - pulse(); + @Test + public void relocateOneSidedLeftConstraintOnly() { + s.setWidth(300); + s.setHeight(200); - assertTrue(s.isShowing()); - assertEquals(120, peer.x, 0.0001); - assertEquals(140, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); - } finally { - toolkit.resetScreens(); - } + // Enable left constraint (10), disable others + var padding = new Insets(-1, -1, -1, 10); + s.relocate(0, 100, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.show(); + pulse(); + + assertEquals(10, peer.x, 0.0001); + assertEquals(100, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), padding); + } + + @Test + public void relocateFlipHorizontalFitsWithoutAdjustment() { + s.setWidth(300); + s.setHeight(200); + + // TOP_LEFT at (790,10) overflows to the right. + // TOP_RIGHT at (790,10) => rawX=790-300=490 fits. + s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.show(); + pulse(); + + assertEquals(490, peer.x, 0.0001); + assertEquals(10, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateAutoDiagonalBeatsAdjustOnly() { + s.setWidth(300); + s.setHeight(200); + + // TOP_LEFT at (790,590) overflows right and bottom. + // AUTO should choose BOTTOM_RIGHT (diagonal flip) => raw=(490,390) fits with no adjustment. + s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, Insets.EMPTY); + s.show(); + pulse(); + + assertEquals(490, peer.x, 0.0001); + assertEquals(390, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateFlipHorizontalStillRequiresVerticalAdjustment() { + s.setWidth(300); + s.setHeight(200); + + // Flip horizontally resolves X, but Y still needs adjustment. + // TOP_RIGHT raw = (490,590) => y clamps to 400. + s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.show(); + pulse(); + + assertEquals(490, peer.x, 0.0001); + assertEquals(400, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } @Test - public void showOnSecondScreenUsesSecondScreenBoundsForClamping() { + public void relocateFlipVerticalStillRequiresHorizontalAdjustment() { + s.setWidth(300); + s.setHeight(200); + + // Flip vertically resolves Y, but X still needs adjustment. + // BOTTOM_LEFT raw = (790,390) => x clamps to 500. + s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_VERTICAL, Insets.EMPTY); + s.show(); + pulse(); + + assertEquals(500, peer.x, 0.0001); + assertEquals(390, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateAutoWithRightOnlyConstraintFlipsHorizontally() { + s.setWidth(300); + s.setHeight(200); + + // Only right edge constrained, others disabled + var constraints = new Insets(-1, 0, -1, -1); + + // Preferred TOP_LEFT: rawX=790 => violates right constraint (maxX=500) + // AUTO should choose TOP_RIGHT: rawX = 790-300 = 490 (fits without adjustment) + s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.show(); + pulse(); + + assertEquals(490, peer.x, 0.0001); + assertEquals(10, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateAutoWithLeftOnlyConstraintDoesNotFlipWhenFlipWouldBeWorse() { + s.setWidth(300); + s.setHeight(200); + + // Only left edge constrained to x >= 10, others disabled + var constraints = new Insets(-1, -1, -1, 10); + + // Preferred TOP_LEFT: rawX = 0 -> adjusted to 10 (cost 10) + // Flipped TOP_RIGHT: rawX = 0-300 = -300 -> adjusted to 10 (cost 310) + // AUTO may consider the flip, but should keep the original anchor as "better". + s.relocate(0, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.show(); + pulse(); + + assertEquals(10, peer.x, 0.0001); + assertEquals(10, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateAutoWithBottomOnlyConstraintFlipsVertically() { + s.setWidth(300); + s.setHeight(200); + + // Only bottom constrained, others disabled + var constraints = new Insets(-1, -1, 0, -1); + + // Preferred TOP_LEFT at y=590 => rawY=590 violates bottom maxY=400 + // Vertical flip to BOTTOM_LEFT yields rawY=590-200=390 (fits) + s.relocate(100, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.show(); + pulse(); + + assertEquals(100, peer.x, 0.0001); + assertEquals(390, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateAutoIgnoresDisabledEdgesWhenDecidingWhetherToFlip() { + s.setWidth(300); + s.setHeight(200); + + // Disable right constraint, enable left constraint (x >= 0). + // This means "overflow to the right is allowed", so AUTO should not flip horizontally + // just because rawX would exceed the screen width. + var constraints = new Insets(-1, -1, -1, 0); + + s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.show(); + pulse(); + + // With only left constraint, rawX=790 is allowed (since right is disabled). + assertEquals(790.0, peer.x, 0.0001); + assertEquals(10.0, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() { + // Make a screen smaller than the stage, so maxX < minX (and maxY < minY). + toolkit.setScreens(new ScreenConfiguration(0, 0, 200, 200, 0, 0, 200, 200, 96)); + s.setWidth(300); + s.setHeight(250); + + // With TOP_LEFT, choose minX/minY in non-fit scenario. + s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + pulse(); + assertEquals(0, peer.x, 0.0001); + assertEquals(0, peer.y, 0.0001); + + // Now recreate with TOP_RIGHT and ensure we choose maxX/minY in non-fit scenario. + s.hide(); + s.setWidth(300); + s.setHeight(250); + s.relocate(0, 0, AnchorPoint.TOP_RIGHT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + pulse(); + + assertEquals(-100, peer.x, 0.0001); // maxX = 200 - 300 = -100 + assertEquals(0, peer.y, 0.0001); // y still chooses minY because TOP_RIGHT has ayRel = 0 + } + + @Test + public void relocateUsesSecondScreenBoundsForConstraints() { toolkit.setScreens( new ScreenConfiguration(0, 0, 1920, 1200, 0, 0, 1920, 1172, 96), new ScreenConfiguration(1920, 160, 1440, 900, 1920, 160, 1440, 900, 96)); - try { - s.setWidth(400); - s.setHeight(300); + s.setWidth(400); + s.setHeight(300); - // Request a position inside the second screen, but would overflow its right/bottom edges. - double requestX = 1920 + 1440 - 10; - double requestY = 160 + 900 - 10; + // Point on 2nd screen, but near its bottom-right corner. + double px = 1920 + 1440 - 1; + double py = 160 + 900 - 1; - s.show(requestX, requestY, AnchorPoint.absolute(0, 0)); - pulse(); + s.relocate(px, py, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + pulse(); - // Expected clamp against *second* screen bounds: - assertEquals(2960, peer.x, 0.0001); - assertEquals(760, peer.y, 0.0001); - assertWithinScreenBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); - } finally { - toolkit.resetScreens(); - } + // Clamp within screen2: x <= 1920+1440-400 = 2960, y <= 160+900-300 = 760 + assertEquals(2960, peer.x, 0.0001); + assertEquals(760, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); + assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); + } + + @Test + public void relocateWithZeroSizeAndProportionalAnchorDoesNotProduceNaNAndConstrainsNormally() { + // Force zero size at positioning time. + s.setWidth(0); + s.setHeight(0); + + // Enable all edges (Insets.EMPTY), so negative coordinate requests are constrained. + s.relocate(-10, -20, AnchorPoint.CENTER, AnchorPolicy.AUTO, Insets.EMPTY); + s.show(); + pulse(); + + // With width/height == 0, maxX == 800, and maxY == 600; raw is (-10, -20) => constrained to (0,0) + assertEquals(0, peer.x, 0.0001); + assertEquals(0, peer.y, 0.0001); + assertFalse(Double.isNaN(peer.x) || Double.isInfinite(peer.x)); + assertFalse(Double.isNaN(peer.y) || Double.isInfinite(peer.y)); + } + + @Test + public void relocateWithZeroSizeAndImpossibleConstraintsChoosesSideUsingAnchorPosition() { + s.setWidth(0); + s.setHeight(0); + + // Make the constrained space impossible even for a 0-size window: + // Horizontal: minX = 500, maxX = 800 - 400 - 0 = 400 => maxX < minX + // Vertical: minY = 300, maxY = 600 - 400 - 0 = 200 => maxY < minY + var constraints = new Insets(300, 400, 400, 500); + + // axRel = 0.25 => choose minX (since axRel <= 0.5) + // ayRel = 0.75 => choose maxY (since ayRel > 0.5) + var anchor = AnchorPoint.proportional(0.25, 0.75); + + s.relocate(0, 0, anchor, AnchorPolicy.FIXED, constraints); + s.show(); + pulse(); + + assertEquals(500, peer.x, 0.0001); + assertEquals(200, peer.y, 0.0001); + } + + @Test + public void relocateWithZeroSizeAndAbsoluteAnchorDoesNotDivideByZero() { + s.setWidth(0); + s.setHeight(0); + + // Force max < min to exercise the "choose side" fallback. + var constraints = new Insets(300, 400, 400, 500); + var anchor = AnchorPoint.absolute(10, 10); + + s.relocate(0, 0, anchor, AnchorPolicy.FIXED, constraints); + s.show(); + pulse(); + + assertEquals(500, peer.x, 0.0001); // minX + assertEquals(300, peer.y, 0.0001); // minY + } + + @ParameterizedTest + @MethodSource("relocateHonorsScreenBounds_arguments") + public void relocateWithFixedAnchorPolicyHonorsScreenBounds( + AnchorPoint anchor, + Insets screenPadding, + double stageW, double stageH, + double requestX, double requestY) { + s.setWidth(stageW); + s.setHeight(stageH); + s.relocate(requestX, requestY, anchor, AnchorPolicy.FIXED, screenPadding); + s.show(); + pulse(); + + assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding); + } + + @ParameterizedTest + @MethodSource("relocateHonorsScreenBoundsWithPadding_arguments") + public void relocateWithFixedAnchorPolicyHonorsScreenBoundsWithPadding( + AnchorPoint anchor, + Insets screenPadding, + double stageW, double stageH, + double requestX, double requestY) { + s.setWidth(stageW); + s.setHeight(stageH); + s.relocate(requestX, requestY, anchor, AnchorPolicy.FIXED, screenPadding); + s.show(); + pulse(); + + assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding); + } + + private static Stream relocateHonorsScreenBounds_arguments() { + return relocateHonorsScreenBounds_argumentsImpl(false); + } + + private static Stream relocateHonorsScreenBoundsWithPadding_arguments() { + return relocateHonorsScreenBounds_argumentsImpl(true); + } + + private static Stream relocateHonorsScreenBounds_argumentsImpl(boolean padding) { + final double screenW = 800; + final double screenH = 600; + final double stageW = 200; + final double stageH = 200; + final double overshoot = 10; // push past the edge to force adjustment + final var insets = padding ? new Insets(10, 20, 30, 40) : Insets.EMPTY; + + Stream.Builder b = Stream.builder(); + b.add(Arguments.of(AnchorPoint.TOP_LEFT, insets, stageW, stageH, + screenW - stageW + overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of(AnchorPoint.TOP_RIGHT, insets, stageW, stageH, + stageW - overshoot, screenH - stageH + overshoot)); + b.add(Arguments.of(AnchorPoint.BOTTOM_LEFT, insets, stageW, stageH, + screenW - stageW + overshoot, stageH - overshoot)); + b.add(Arguments.of(AnchorPoint.BOTTOM_RIGHT, insets, stageW, stageH, + stageW - overshoot, stageH - overshoot)); + return b.build(); } - private static void assertWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { - assertTrue(isWithinScreenBounds(peer, screen, padding), "Stage is not within screen bounds"); + private static void assertWithinBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + assertTrue(isWithinBounds(peer, screen, padding), "Stage is not within bounds"); } - private static void assertNotWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { - assertFalse(isWithinScreenBounds(peer, screen, padding), "Stage is within screen bounds"); + private static void assertNotWithinBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + assertFalse(isWithinBounds(peer, screen, padding), "Stage is within bounds"); } - private static boolean isWithinScreenBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { + private static boolean isWithinBounds(StubStage peer, ScreenConfiguration screen, Insets padding) { return screen.getMinX() + padding.getLeft() <= peer.x && screen.getMinY() + padding.getTop() <= peer.y && screen.getMinX() + screen.getWidth() - padding.getRight() >= peer.x + peer.width From 5ca15d3b3fd4fe13efc58cd01a3e5b20cfa831c7 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 27 Nov 2025 23:37:32 +0100 Subject: [PATCH 08/12] tweaks --- .../src/main/java/javafx/geometry/AnchorPoint.java | 5 +++++ .../src/main/java/javafx/stage/AnchorPolicy.java | 2 +- .../javafx.graphics/src/main/java/javafx/stage/Stage.java | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java index 172295d27a9..66de156b310 100644 --- a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java +++ b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java @@ -123,6 +123,11 @@ public int hashCode() { return hash; } + @Override + public String toString() { + return "AnchorPoint [x = " + x + ", y = " + y + ", proportional = " + proportional + "]"; + } + /** * Anchor at the top-left corner of the target area. *

diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java index 0a9ec0c2ad2..a161a393307 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -91,4 +91,4 @@ public enum AnchorPolicy { * If this is not possible, the window is biased towards the edge that is closer to the anchor. */ AUTO -} \ No newline at end of file +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 65e2b6aa0a6..cc6f735bf45 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -1267,7 +1267,7 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then *

    - *
  1. any previous call to {@link #centerOnScreen()} is canceled, + *
  2. any previous call to {@link #centerOnScreen()} is disregarded, *
  3. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated * immediately; instead, they are updated after the stage is shown. *
From 007d485ed015ef29224a2e37072c862285dacfe8 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:38:30 +0100 Subject: [PATCH 09/12] doc fixes --- .../java/com/sun/javafx/stage/WindowBoundsUtil.java | 2 +- .../src/main/java/javafx/geometry/AnchorPoint.java | 4 ++-- .../src/main/java/javafx/stage/AnchorPolicy.java | 10 +++++----- .../src/main/java/javafx/stage/Stage.java | 11 ++++++----- .../src/test/java/test/javafx/stage/StageTest.java | 12 ++++++------ 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java index dbfd98dbb3b..0937cdb57d6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java @@ -124,7 +124,7 @@ public static Point2D computeAdjustedLocation(double screenX, double screenY, *

* For each inset value: *

    - *
  • {@code >= 0} enables a constraint for that edge and contributes to the usable region + *
  • {@code >= 0} enables a constraint for that edge *
  • {@code < 0} disables the constraint for that edge *
* Enabled constraints shrink the usable region by the given amounts. The computed {@code maxX} diff --git a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java index 66de156b310..91418c01ef6 100644 --- a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java +++ b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java @@ -32,10 +32,10 @@ * An {@code AnchorPoint} provides a {@code (x, y)} coordinate together with a flag indicating * how those coordinates should be interpreted: *
    - *
  • Proportional: {@code x} and {@code y} are expressed as fractions of the target width and height. + *
  • Proportional: {@code x} and {@code y} are expressed as fractions of the target width and height. * In this coordinate system, {@code (0, 0)} refers to the top-left corner, and {@code (1, 1)} refers to * the bottom-right corner. Values outside the {@code [0..1]} range represent points outside the bounds. - *
  • Absolute: {@code x} and {@code y} are expressed as offsets in pixels from the top-left corner + *
  • Absolute: {@code x} and {@code y} are expressed as offsets in pixels from the top-left corner * of the target area. *
* diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java index a161a393307..dd6b3dd93cf 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -29,13 +29,13 @@ import javafx.geometry.Insets; /** - * Specifies how a repositioning operation may adjust an anchor point when the preferred placement - * would violate the screen bounds constraints. + * Specifies how a window repositioning operation may adjust an anchor point when the preferred anchor + * would place the window outside the usable screen area. *

* The anchor passed to {@link Stage#relocate(double, double, AnchorPoint, AnchorPolicy, Insets)} or specified * by {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point on the * window that should coincide with the requested screen coordinates. When the preferred anchor would place - * the window outside the allowed screen area (as defined by the screen bounds and any configured insets), + * the window outside the usable screen area (as defined by the screen bounds and any configured insets), * an {@code AnchorPolicy} can be used to select an alternative anchor before applying any final position * adjustment. * @@ -53,7 +53,7 @@ public enum AnchorPolicy { FIXED, /** - * If the preferred placement violates horizontal constraints, attempt a horizontally flipped anchor. + * If the preferred anchor violates horizontal constraints, attempt a horizontally flipped anchor. *

* A horizontal flip mirrors the anchor across the vertical center line of the window * (for example, {@code TOP_LEFT} becomes {@code TOP_RIGHT}). @@ -65,7 +65,7 @@ public enum AnchorPolicy { FLIP_HORIZONTAL, /** - * If the preferred placement violates vertical constraints, attempt a vertically flipped anchor. + * If the preferred anchor violates vertical constraints, attempt a vertically flipped anchor. *

* A vertical flip mirrors the anchor across the horizontal center line of the window * (for example, {@code TOP_LEFT} becomes {@code BOTTOM_LEFT}). diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index cc6f735bf45..883131cfd5d 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -1227,7 +1227,7 @@ public void toBack() { * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then *

    - *
  1. any previous call to {@link #centerOnScreen()} is canceled, + *
  2. any previous call to {@link #centerOnScreen()} is disregarded, *
  3. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated * immediately; instead, they are updated after the stage is shown. *
@@ -1250,12 +1250,13 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { *

* The {@code anchor} identifies a point on the stage that should coincide with the requested screen * coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is then - * optionally adjusted to keep the stage within screen edge constraints. + * optionally adjusted to keep the stage within the usable screen area. *

* The {@code anchorPolicy} controls whether an alternative anchor may be used when the preferred anchor - * would violate screen edge constraints. Depending on the policy, the preferred anchor location may be - * mirrored to the other side of the window horizontally or vertically, or an anchor might be selected - * automatically. If no alternative anchor yields a better placement, the specified {@code anchor} is used. + * would place the stage outside the usable screen area. Depending on the policy, the preferred anchor + * location may be mirrored to the other side of the window horizontally or vertically, or an anchor might + * be selected automatically. If no alternative anchor yields a better placement, the specified + * {@code anchor} is used. *

* The {@code screenPadding} parameter defines per-edge constraints against the current screen bounds. * Each inset value specifies the minimum distance to maintain between the stage edge and the corresponding diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 65fbf53c411..7b98929ad36 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -822,7 +822,7 @@ public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() pulse(); assertEquals(-100, peer.x, 0.0001); // maxX = 200 - 300 = -100 - assertEquals(0, peer.y, 0.0001); // y still chooses minY because TOP_RIGHT has ayRel = 0 + assertEquals(0, peer.y, 0.0001); // choose minY because TOP_RIGHT has y = 0 } @Test @@ -834,7 +834,7 @@ public void relocateUsesSecondScreenBoundsForConstraints() { s.setWidth(400); s.setHeight(300); - // Point on 2nd screen, but near its bottom-right corner. + // Point on screen 2, but near its bottom-right corner. double px = 1920 + 1440 - 1; double py = 160 + 900 - 1; @@ -842,7 +842,7 @@ public void relocateUsesSecondScreenBoundsForConstraints() { s.show(); pulse(); - // Clamp within screen2: x <= 1920+1440-400 = 2960, y <= 160+900-300 = 760 + // Clamp within screen 2: x <= 1920+1440-400 = 2960, y <= 160+900-300 = 760 assertEquals(2960, peer.x, 0.0001); assertEquals(760, peer.y, 0.0001); assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); @@ -872,13 +872,13 @@ public void relocateWithZeroSizeAndImpossibleConstraintsChoosesSideUsingAnchorPo s.setWidth(0); s.setHeight(0); - // Make the constrained space impossible even for a 0-size window: + // Make the constrained space impossible even for a zero-size window: // Horizontal: minX = 500, maxX = 800 - 400 - 0 = 400 => maxX < minX // Vertical: minY = 300, maxY = 600 - 400 - 0 = 200 => maxY < minY var constraints = new Insets(300, 400, 400, 500); - // axRel = 0.25 => choose minX (since axRel <= 0.5) - // ayRel = 0.75 => choose maxY (since ayRel > 0.5) + // x = 0.25 => choose minX (since x <= 0.5) + // y = 0.75 => choose maxY (since y > 0.5) var anchor = AnchorPoint.proportional(0.25, 0.75); s.relocate(0, 0, anchor, AnchorPolicy.FIXED, constraints); From 8d9138b795ebbb63c55f79002ed65e00c52c8262 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:33:04 +0100 Subject: [PATCH 10/12] small changes --- ...owBoundsUtil.java => WindowRelocator.java} | 43 +++--- .../java/javafx/geometry/AnchorPoint.java | 135 +++++++++--------- .../main/java/javafx/stage/AnchorPolicy.java | 34 +++-- .../main/java/javafx/stage/PopupWindow.java | 8 +- .../src/main/java/javafx/stage/Stage.java | 15 +- 5 files changed, 117 insertions(+), 118 deletions(-) rename modules/javafx.graphics/src/main/java/com/sun/javafx/stage/{WindowBoundsUtil.java => WindowRelocator.java} (90%) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java similarity index 90% rename from modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java rename to modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java index 0937cdb57d6..7b23ad8dfdc 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowBoundsUtil.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java @@ -37,22 +37,24 @@ import java.util.Objects; import java.util.function.Consumer; -public final class WindowBoundsUtil { +public final class WindowRelocator { - private WindowBoundsUtil() {} + private WindowRelocator() {} /** - * Creates a relocation operation that positions a {@link Window} at the requested screen coordinates + * Creates a relocator that positions a {@link Window} at the requested screen coordinates * using an {@link AnchorPoint}, {@link AnchorPolicy}, and per-edge screen constraints. *

* Screen edge constraints are specified by {@code screenPadding}: - * values {@code >= 0} enable a constraint for the corresponding edge (minimum distance to keep), - * values {@code < 0} disable the constraint for that edge. Enabled constraints reduce the usable area - * for placement by the given insets. + *

    + *
  • values {@code >= 0} enable a constraint for the corresponding edge (minimum distance to keep) + *
  • values {@code < 0} disable the constraint for that edge + *
+ * Enabled constraints reduce the usable area for placement by the given insets. */ - public static Consumer newDeferredRelocation(double screenX, double screenY, - AnchorPoint anchor, AnchorPolicy anchorPolicy, - Insets screenPadding) { + public static Consumer newDeferredRelocator(double screenX, double screenY, + AnchorPoint anchor, AnchorPolicy anchorPolicy, + Insets screenPadding) { Objects.requireNonNull(anchor, "anchor cannot be null"); Objects.requireNonNull(anchorPolicy, "anchorPolicy cannot be null"); Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); @@ -271,19 +273,16 @@ private static boolean isVerticalValid(Position raw, Constraints c) { private static AnchorPoint flipAnchor(AnchorPoint anchor, double width, double height, boolean flipH, boolean flipV) { - if (anchor.isProportional()) { - double x = anchor.getX(); - double y = anchor.getY(); - double nx = flipH ? (1.0 - x) : x; - double ny = flipV ? (1.0 - y) : y; - return AnchorPoint.proportional(nx, ny); - } else { - double x = anchor.getX(); - double y = anchor.getY(); - double nx = flipH ? (width - x) : x; - double ny = flipV ? (height - y) : y; - return AnchorPoint.absolute(nx, ny); - } + double x = anchor.getX(); + double y = anchor.getY(); + + return anchor.isProportional() + ? AnchorPoint.proportional( + flipH ? (1.0 - x) : x, + flipV ? (1.0 - y) : y) + : AnchorPoint.absolute( + flipH ? (width - x) : x, + flipV ? (height - y) : y); } private record Constraints(boolean hasMinX, boolean hasMaxX, diff --git a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java index 91418c01ef6..abb267c0745 100644 --- a/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java +++ b/modules/javafx.graphics/src/main/java/javafx/geometry/AnchorPoint.java @@ -35,14 +35,76 @@ *
  • Proportional: {@code x} and {@code y} are expressed as fractions of the target width and height. * In this coordinate system, {@code (0, 0)} refers to the top-left corner, and {@code (1, 1)} refers to * the bottom-right corner. Values outside the {@code [0..1]} range represent points outside the bounds. - *
  • Absolute: {@code x} and {@code y} are expressed as offsets in pixels from the top-left corner - * of the target area. + *
  • Absolute: {@code x} and {@code y} are expressed as offsets from the top-left corner of the target area. * * * @since 26 */ public final class AnchorPoint { + /** + * Anchor at the top-left corner of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0, 0)}. + */ + public static final AnchorPoint TOP_LEFT = new AnchorPoint(0, 0, true); + + /** + * Anchor at the top-center midpoint of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0)}. + */ + public static final AnchorPoint TOP_CENTER = new AnchorPoint(0.5, 0, true); + + /** + * Anchor at the top-right corner of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(1, 0)}. + */ + public static final AnchorPoint TOP_RIGHT = new AnchorPoint(1, 0, true); + + /** + * Anchor at the center-left midpoint of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0, 0.5)}. + */ + public static final AnchorPoint CENTER_LEFT = new AnchorPoint(0, 0.5, true); + + /** + * Anchor at the center of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0.5)}. + */ + public static final AnchorPoint CENTER = new AnchorPoint(0.5, 0.5, true); + + /** + * Anchor at the center-right midpoint of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(1, 0.5)}. + */ + public static final AnchorPoint CENTER_RIGHT = new AnchorPoint(1, 0.5, true); + + /** + * Anchor at the bottom-left corner of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0, 1)}. + */ + public static final AnchorPoint BOTTOM_LEFT = new AnchorPoint(0, 1, true); + + /** + * Anchor at the bottom-center midpoint of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 1)}. + */ + public static final AnchorPoint BOTTOM_CENTER = new AnchorPoint(0.5, 1, true); + + /** + * Anchor at the bottom-right corner of the target area. + *

    + * This constant is equivalent to {@code AnchorPoint.proportional(1, 1)}. + */ + public static final AnchorPoint BOTTOM_RIGHT = new AnchorPoint(1, 1, true); + private final double x; private final double y; private final boolean proportional; @@ -69,10 +131,10 @@ public static AnchorPoint proportional(double x, double y) { } /** - * Creates an absolute anchor point, expressed as pixel offsets from the top-left corner of the target area. + * Creates an absolute anchor point, expressed as offsets from the top-left corner of the target area. * - * @param x the horizontal offset in pixels from the left edge of the target area - * @param y the vertical offset in pixels from the top edge of the target area + * @param x the horizontal offset from the left edge of the target area + * @param y the vertical offset from the top edge of the target area * @return an absolute {@code AnchorPoint} */ public static AnchorPoint absolute(double x, double y) { @@ -127,67 +189,4 @@ public int hashCode() { public String toString() { return "AnchorPoint [x = " + x + ", y = " + y + ", proportional = " + proportional + "]"; } - - /** - * Anchor at the top-left corner of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0, 0)}. - */ - public static final AnchorPoint TOP_LEFT = new AnchorPoint(0, 0, true); - - /** - * Anchor at the top-center midpoint of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0)}. - */ - public static final AnchorPoint TOP_CENTER = new AnchorPoint(0.5, 0, true); - - /** - * Anchor at the top-right corner of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(1, 0)}. - */ - public static final AnchorPoint TOP_RIGHT = new AnchorPoint(1, 0, true); - - /** - * Anchor at the center-left midpoint of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0, 0.5)}. - */ - public static final AnchorPoint CENTER_LEFT = new AnchorPoint(0, 0.5, true); - - /** - * Anchor at the center of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 0.5)}. - */ - public static final AnchorPoint CENTER = new AnchorPoint(0.5, 0.5, true); - - /** - * Anchor at the center-right midpoint of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(1, 0.5)}. - */ - public static final AnchorPoint CENTER_RIGHT = new AnchorPoint(1, 0.5, true); - - /** - * Anchor at the bottom-left corner of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0, 1)}. - */ - public static final AnchorPoint BOTTOM_LEFT = new AnchorPoint(0, 1, true); - - /** - * Anchor at the bottom-center midpoint of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(0.5, 1)}. - */ - public static final AnchorPoint BOTTOM_CENTER = new AnchorPoint(0.5, 1, true); - - /** - * Anchor at the bottom-right corner of the target area. - *

    - * This constant is equivalent to {@code AnchorPoint.proportional(1, 1)}. - */ - public static final AnchorPoint BOTTOM_RIGHT = new AnchorPoint(1, 1, true); } diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java index dd6b3dd93cf..f80d19594a2 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -46,33 +46,31 @@ public enum AnchorPolicy { /** * Always use the preferred anchor and never select an alternative anchor. *

    - * If the preferred placement violates the allowed screen area, the window position is adjusted - * for the window to fall within the allowed screen area. If this is not possible, the window is - * biased towards the edge that is closer to the anchor. + * If the preferred anchor places the window outside the usable screen area, the window position is + * adjusted for the window to fall within the usable screen area. If this is not possible, the window + * is biased towards the edge that is closer to the anchor. */ FIXED, /** * If the preferred anchor violates horizontal constraints, attempt a horizontally flipped anchor. *

    - * A horizontal flip mirrors the anchor across the vertical center line of the window - * (for example, {@code TOP_LEFT} becomes {@code TOP_RIGHT}). - *

    - * If the horizontally flipped anchor does not improve the placement, the original anchor is used - * and the final position is adjusted for the window to fall within the allowed screen area. - * If this is not possible, the window is biased towards the edge that is closer to the anchor. + * A horizontal flip mirrors the anchor across the vertical center line of the window (for example, + * {@code TOP_LEFT} becomes {@code TOP_RIGHT}). If the horizontally flipped anchor does not improve + * the placement, the original anchor is used and the final position is adjusted for the window to + * fall within the usable screen area. If this is not possible, the window is biased towards the + * edge that is closer to the anchor. */ FLIP_HORIZONTAL, /** * If the preferred anchor violates vertical constraints, attempt a vertically flipped anchor. *

    - * A vertical flip mirrors the anchor across the horizontal center line of the window - * (for example, {@code TOP_LEFT} becomes {@code BOTTOM_LEFT}). - *

    - * If the vertically flipped anchor does not improve the placement, the original anchor is used - * and the final position is adjusted for the window to fall within the allowed screen area. - * If this is not possible, the window is biased towards the edge that is closer to the anchor. + * A vertical flip mirrors the anchor across the horizontal center line of the window (for example, + * {@code TOP_LEFT} becomes {@code BOTTOM_LEFT}). If the vertically flipped anchor does not improve + * the placement, the original anchor is used and the final position is adjusted for the window to + * fall within the usable screen area. If this is not possible, the window is biased towards the + * edge that is closer to the anchor. */ FLIP_VERTICAL, @@ -83,11 +81,11 @@ public enum AnchorPolicy { *

      *
    • If only horizontal constraints are violated, it behaves like {@link #FLIP_HORIZONTAL}. *
    • If only vertical constraints are violated, it behaves like {@link #FLIP_VERTICAL}. - *
    • If both horizontal and vertical constraints are violated, it may attempt a diagonal flip - * (horizontal and vertical) to keep the window on the opposite side of the requested point. + *
    • If both horizontal and vertical constraints are violated, it attempts a diagonal flip, + * then a horizontal flip, and finally a vertical flip. *
    * If no alternative anchor yields a better placement, the original anchor is used and the final - * position is adjusted for the window to fall within the allowed screen area. + * position is adjusted for the window to fall within the usable screen area. * If this is not possible, the window is biased towards the edge that is closer to the anchor. */ AUTO diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java index b47978fc294..1e2ea8bd9e7 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java @@ -30,6 +30,7 @@ import com.sun.javafx.event.DirectEvent; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -62,9 +63,9 @@ import com.sun.javafx.scene.SceneHelper; import com.sun.javafx.stage.FocusUngrabEvent; import com.sun.javafx.stage.PopupWindowPeerListener; -import com.sun.javafx.stage.WindowBoundsUtil; import com.sun.javafx.stage.WindowCloseRequestHandler; import com.sun.javafx.stage.WindowEventDispatcher; +import com.sun.javafx.stage.WindowRelocator; import com.sun.javafx.tk.Toolkit; import com.sun.javafx.stage.PopupWindowHelper; @@ -813,11 +814,12 @@ private void updateWindow(final double newAnchorX, ? currentScreen.getBounds() : currentScreen.getVisualBounds(); - Point2D location = WindowBoundsUtil.computeAdjustedLocation( + Point2D location = WindowRelocator.computeAdjustedLocation( newAnchorX, newAnchorY, anchorBounds.getWidth(), anchorBounds.getHeight(), AnchorPoint.proportional(anchorXCoef, anchorYCoef), - getAnchorPolicy(), screenBounds, Insets.EMPTY); + Objects.requireNonNullElse(getAnchorPolicy(), AnchorPolicy.FIXED), + screenBounds, Insets.EMPTY); anchorScrMinX = location.getX(); anchorScrMinY = location.getY(); diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 883131cfd5d..f472e9d652c 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -53,7 +53,7 @@ import com.sun.javafx.stage.HeaderButtonMetrics; import com.sun.javafx.stage.StageHelper; import com.sun.javafx.stage.StagePeerListener; -import com.sun.javafx.stage.WindowBoundsUtil; +import com.sun.javafx.stage.WindowRelocator; import com.sun.javafx.tk.TKStage; import com.sun.javafx.tk.Toolkit; import javafx.beans.NamedArg; @@ -1246,11 +1246,11 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { /** * Moves this stage to the specified screen location using the given anchor, anchor-selection policy, - * and screen edge constraints. + * and screen edge constraints that may restrict the usable screen area. *

    - * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is then - * optionally adjusted to keep the stage within the usable screen area. + * The {@code anchor} identifies a point in stage coordinates that should coincide with the requested + * screen coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is + * then optionally adjusted to keep the stage within the usable screen area. *

    * The {@code anchorPolicy} controls whether an alternative anchor may be used when the preferred anchor * would place the stage outside the usable screen area. Depending on the policy, the preferred anchor @@ -1275,7 +1275,8 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { * * @param anchorX the requested horizontal location of the anchor point on the screen * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen + * @param anchor the point in stage coordinates that should coincide with {@code (anchorX, anchorY)} + * in screen coordinates * @param anchorPolicy controls alternative anchor selection if the preferred placement violates * enabled screen constraints * @param screenPadding per-edge minimum distance constraints relative to the screen edges; @@ -1285,7 +1286,7 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { */ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor, AnchorPolicy anchorPolicy, Insets screenPadding) { - var request = WindowBoundsUtil.newDeferredRelocation(anchorX, anchorY, anchor, anchorPolicy, screenPadding); + var request = WindowRelocator.newDeferredRelocator(anchorX, anchorY, anchor, anchorPolicy, screenPadding); if (isShowing()) { request.accept(this); From a20128713cf0f5f620611cf9d09a6494117f0539 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:32:11 +0100 Subject: [PATCH 11/12] refactor --- .../com/sun/javafx/stage/WindowHelper.java | 7 +- .../javafx/stage/WindowLocationAlgorithm.java | 37 +++++ .../com/sun/javafx/stage/WindowRelocator.java | 61 ++++++-- .../main/java/com/sun/javafx/util/Utils.java | 19 +++ .../main/java/javafx/stage/AnchorPolicy.java | 8 +- .../main/java/javafx/stage/PopupWindow.java | 8 +- .../src/main/java/javafx/stage/Stage.java | 105 +++++++------ .../src/main/java/javafx/stage/Window.java | 138 +++++++++--------- .../java/test/javafx/stage/StageTest.java | 124 +++++++++------- 9 files changed, 308 insertions(+), 199 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowLocationAlgorithm.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowHelper.java index a4393c7664f..a98fc99b7b6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowHelper.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -90,6 +90,10 @@ protected void visibleChangedImpl(Window window, boolean visible) { * Methods used by Window (base) class only */ + public static Window getWindowOwner(Window window) { + return windowAccessor.getWindowOwner(window); + } + public static TKStage getPeer(Window window) { return windowAccessor.getPeer(window); } @@ -145,6 +149,7 @@ public interface WindowAccessor { void setHelper(Window window, WindowHelper windowHelper); void doVisibleChanging(Window window, boolean visible); void doVisibleChanged(Window window, boolean visible); + Window getWindowOwner(Window window); TKStage getPeer(Window window); void setPeer(Window window, TKStage peer); WindowPeerListener getPeerListener(Window window); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowLocationAlgorithm.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowLocationAlgorithm.java new file mode 100644 index 00000000000..83a09d94a64 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowLocationAlgorithm.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.stage; + +import javafx.stage.Screen; + +public interface WindowLocationAlgorithm { + + record ComputedLocation( + double x, double y, + double xGravity, double yGravity) {} + + ComputedLocation compute(Screen screen, double windowWidth, double windowHeight); +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java index 7b23ad8dfdc..ea48ec0c3fd 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java @@ -35,15 +35,14 @@ import javafx.stage.Window; import java.util.List; import java.util.Objects; -import java.util.function.Consumer; public final class WindowRelocator { private WindowRelocator() {} /** - * Creates a relocator that positions a {@link Window} at the requested screen coordinates - * using an {@link AnchorPoint}, {@link AnchorPolicy}, and per-edge screen constraints. + * Creates a location algorithm that computes the position of the {@link Window} at the requested screen + * coordinates using an {@link AnchorPoint}, {@link AnchorPolicy}, and per-edge screen constraints. *

    * Screen edge constraints are specified by {@code screenPadding}: *

      @@ -52,27 +51,57 @@ private WindowRelocator() {} *
    * Enabled constraints reduce the usable area for placement by the given insets. */ - public static Consumer newDeferredRelocator(double screenX, double screenY, - AnchorPoint anchor, AnchorPolicy anchorPolicy, - Insets screenPadding) { - Objects.requireNonNull(anchor, "anchor cannot be null"); + public static WindowLocationAlgorithm newRelocationAlgorithm(AnchorPoint screenAnchor, + AnchorPoint stageAnchor, + AnchorPolicy anchorPolicy, + Insets screenPadding) { + Objects.requireNonNull(screenAnchor, "screenAnchor cannot be null"); + Objects.requireNonNull(stageAnchor, "stageAnchor cannot be null"); Objects.requireNonNull(anchorPolicy, "anchorPolicy cannot be null"); Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); - return window -> { - Screen currentScreen = Utils.getScreenForPoint(screenX, screenY); - Rectangle2D screenBounds = Utils.hasFullScreenStage(currentScreen) - ? currentScreen.getBounds() - : currentScreen.getVisualBounds(); + return (windowScreen, windowWidth, windowHeight) -> { + double screenX, screenY; + double gravityX, gravityY; + + // Compute the absolute coordinates of the screen anchor. + // If the screen anchor is specified in proportional coordinates, it refers to the complete + // screen bounds when a full-screen stage is showing on the screen, and the visual bounds otherwise. + if (screenAnchor.isProportional()) { + Rectangle2D bounds = Utils.hasFullScreenStage(windowScreen) + ? windowScreen.getBounds() + : windowScreen.getVisualBounds(); + + screenX = bounds.getMinX() + screenAnchor.getX() * bounds.getWidth(); + screenY = bounds.getMinY() + screenAnchor.getY() * bounds.getHeight(); + } else { + screenX = screenAnchor.getX(); + screenY = screenAnchor.getY(); + } + + // The absolute screen anchor might be on a different screen than the current window, so we + // need to recompute the actual screen and its bounds (complete when full-screen stage showing, + // visual otherwise). + Screen targetScreen = Utils.getScreenForPoint(screenX, screenY); + Rectangle2D screenBounds = Utils.hasFullScreenStage(targetScreen) + ? targetScreen.getBounds() + : targetScreen.getVisualBounds(); Point2D location = computeAdjustedLocation( screenX, screenY, - window.getWidth(), window.getHeight(), - anchor, anchorPolicy, + windowWidth, windowHeight, + stageAnchor, anchorPolicy, screenBounds, screenPadding); - window.setX(location.getX()); - window.setY(location.getY()); + if (stageAnchor.isProportional()) { + gravityX = stageAnchor.getX(); + gravityY = stageAnchor.getY(); + } else { + gravityX = stageAnchor.getX() / windowWidth; + gravityY = stageAnchor.getY() / windowHeight; + } + + return new WindowLocationAlgorithm.ComputedLocation(location.getX(), location.getY(), gravityX, gravityY); }; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java index 5f093e8eec1..f0a4b0da990 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java @@ -46,6 +46,7 @@ import java.math.BigDecimal; import java.util.List; import com.sun.javafx.PlatformUtil; +import com.sun.javafx.stage.WindowHelper; import com.sun.glass.utils.NativeLibLoader; import com.sun.prism.impl.PrismSettings; @@ -746,6 +747,24 @@ public static Screen getScreen(Object obj) { return getScreenForRectangle(rect); } + public static Screen getScreenForWindow(Window window) { + do { + if (!Double.isNaN(window.getX()) && !Double.isNaN(window.getY())) { + if (window.getWidth() >= 0 && window.getHeight() >= 0) { + var bounds = new Rectangle2D(window.getX(), window.getY(), + window.getWidth(), window.getHeight()); + return getScreenForRectangle(bounds); + } else { + return getScreenForPoint(window.getX(), window.getY()); + } + } + + window = WindowHelper.getWindowOwner(window); + } while (window != null); + + return Screen.getPrimary(); + } + public static Screen getScreenForRectangle(final Rectangle2D rect) { final List screens = Screen.getScreens(); diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java index f80d19594a2..3e1b4638311 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -32,10 +32,10 @@ * Specifies how a window repositioning operation may adjust an anchor point when the preferred anchor * would place the window outside the usable screen area. *

    - * The anchor passed to {@link Stage#relocate(double, double, AnchorPoint, AnchorPolicy, Insets)} or specified - * by {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point on the - * window that should coincide with the requested screen coordinates. When the preferred anchor would place - * the window outside the usable screen area (as defined by the screen bounds and any configured insets), + * The stage anchor passed to {@link Stage#relocate(AnchorPoint, AnchorPoint, AnchorPolicy, Insets)} or + * specified by {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point + * on the window that should coincide with the requested screen coordinates. When the preferred anchor would + * place the window outside the usable screen area (as defined by the screen bounds and any configured insets), * an {@code AnchorPolicy} can be used to select an alternative anchor before applying any final position * adjustment. * diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java index 1e2ea8bd9e7..7c97f2994d9 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/PopupWindow.java @@ -752,12 +752,12 @@ boolean isContentLocation() { } @Override - void setXInternal(final double value) { + void setXInternal(double value, float xGravity) { updateWindow(windowToAnchorX(value), getAnchorY()); } @Override - void setYInternal(final double value) { + void setYInternal(double value, float yGravity) { updateWindow(getAnchorX(), windowToAnchorY(value)); } @@ -842,11 +842,11 @@ private void updateWindow(final double newAnchorX, // update popup position // don't set Window.xExplicit unnecessarily if (!Double.isNaN(windowScrMinX)) { - super.setXInternal(windowScrMinX); + super.setXInternal(windowScrMinX, 0); } // don't set Window.yExplicit unnecessarily if (!Double.isNaN(windowScrMinY)) { - super.setYInternal(windowScrMinY); + super.setYInternal(windowScrMinY, 0); } // set anchor x, anchor y diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index f472e9d652c..1fd906c23e1 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -53,6 +52,7 @@ import com.sun.javafx.stage.HeaderButtonMetrics; import com.sun.javafx.stage.StageHelper; import com.sun.javafx.stage.StagePeerListener; +import com.sun.javafx.stage.WindowLocationAlgorithm; import com.sun.javafx.stage.WindowRelocator; import com.sun.javafx.tk.TKStage; import com.sun.javafx.tk.Toolkit; @@ -1218,11 +1218,8 @@ public void toBack() { } /** - * Moves this stage to the specified screen location using the given anchor. - *

    - * The {@code anchor} identifies a point on the stage that should coincide with the requested screen - * coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is then - * adjusted to keep the stage within the screen bounds. + * Positions this stage so that a point on the stage ({@code stageAnchor}) coincides with a point on + * the screen ({@code screenAnchor}), and ensures that the stage is not placed off-screen. *

    * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then @@ -1232,38 +1229,25 @@ public void toBack() { * immediately; instead, they are updated after the stage is shown. * * Calling this method is equivalent to calling - * {@code relocate(anchorX, anchorY, anchor, AnchorPolicy.FIXED, Insets.EMPTY)}. + * {@code relocate(screenAnchor, stageAnchor, AnchorPolicy.FIXED, Insets.EMPTY)}. * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point on the stage that should coincide with {@code (anchorX, anchorY)} on the screen - * @throws NullPointerException if {@code anchor} is {@code null} + * @param screenAnchor An anchor point in absolute or proportional screen coordinates. If the screen anchor + * is {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first + * resolved against the {@linkplain Screen#getVisualBounds() visual bounds} of the screen + * that currently contains this stage; if the stage does not have a location yet, the + * primary screen is used. If a full-screen stage is showing on the screen, the screen + * anchor is resolved against its complete {@linkplain Screen#getBounds() bounds}. + * @param stageAnchor An anchor point in absolute or proportional stage coordinates. + * @throws NullPointerException if any of the parameters is {@code null} * @since 26 */ - public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { - relocate(anchorX, anchorY, anchor, AnchorPolicy.FIXED, Insets.EMPTY); + public final void relocate(AnchorPoint screenAnchor, AnchorPoint stageAnchor) { + relocate(screenAnchor, stageAnchor, AnchorPolicy.FIXED, Insets.EMPTY); } /** - * Moves this stage to the specified screen location using the given anchor, anchor-selection policy, - * and screen edge constraints that may restrict the usable screen area. - *

    - * The {@code anchor} identifies a point in stage coordinates that should coincide with the requested - * screen coordinates {@code (anchorX, anchorY)}. The stage location is derived from this anchor and is - * then optionally adjusted to keep the stage within the usable screen area. - *

    - * The {@code anchorPolicy} controls whether an alternative anchor may be used when the preferred anchor - * would place the stage outside the usable screen area. Depending on the policy, the preferred anchor - * location may be mirrored to the other side of the window horizontally or vertically, or an anchor might - * be selected automatically. If no alternative anchor yields a better placement, the specified - * {@code anchor} is used. - *

    - * The {@code screenPadding} parameter defines per-edge constraints against the current screen bounds. - * Each inset value specifies the minimum distance to maintain between the stage edge and the corresponding - * screen edge. A value {@code >= 0} enables the corresponding edge constraint; a negative value disables - * the constraint for that edge. Enabled constraints effectively shrink the usable screen area by the - * given insets. For example, a left inset of {@code 10} ensures the stage will not be placed closer than - * 10 pixels to the left screen edge. + * Positions this stage so that a point on the stage ({@code stageAnchor}) coincides with a point on + * the screen ({@code screenAnchor}), subject to the specified anchor policy and screen padding. *

    * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then @@ -1273,34 +1257,41 @@ public final void relocate(double anchorX, double anchorY, AnchorPoint anchor) { * immediately; instead, they are updated after the stage is shown. * * - * @param anchorX the requested horizontal location of the anchor point on the screen - * @param anchorY the requested vertical location of the anchor point on the screen - * @param anchor the point in stage coordinates that should coincide with {@code (anchorX, anchorY)} - * in screen coordinates - * @param anchorPolicy controls alternative anchor selection if the preferred placement violates - * enabled screen constraints - * @param screenPadding per-edge minimum distance constraints relative to the screen edges; - * values {@code < 0} disable the constraint for the respective edge - * @throws NullPointerException if {@code anchor}, {@code anchorPolicy}, or {@code screenPadding} is {@code null} + * @param screenAnchor An anchor point in absolute or proportional screen coordinates. If the screen anchor + * is {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first + * resolved against the {@linkplain Screen#getVisualBounds() visual bounds} of the screen + * that currently contains this stage; if the stage does not have a location yet, the + * primary screen is used. If a full-screen stage is showing on the screen, the screen + * anchor is resolved against its complete {@linkplain Screen#getBounds() bounds}. + * @param stageAnchor An anchor point in absolute or proportional stage coordinates. + * @param anchorPolicy Controls whether an alternative stage anchor may be used when the preferred anchor would + * place the stage outside the usable screen area. Depending on the policy, the preferred + * anchor location may be mirrored across the vertical/horizontal center line of the stage, + * or an anchor might be selected automatically. If no alternative anchor yields a better + * placement, the specified {@code stageAnchor} is used. + * @param screenPadding Defines per-edge constraints against the screen bounds. Each inset value specifies the + * minimum distance to maintain between the stage edge and the corresponding screen edge. + * A value {@code >= 0} enables the corresponding edge constraint; a negative value disables + * the constraint for that edge. Enabled constraints effectively shrink the usable screen + * area by the given insets. For example, a left inset of {@code 10} ensures the stage will + * not be placed closer than 10 pixels to the left screen edge. + * @throws NullPointerException if any of the parameters is {@code null} * @since 26 */ - public final void relocate(double anchorX, double anchorY, AnchorPoint anchor, + public final void relocate(AnchorPoint screenAnchor, AnchorPoint stageAnchor, AnchorPolicy anchorPolicy, Insets screenPadding) { - var request = WindowRelocator.newDeferredRelocator(anchorX, anchorY, anchor, anchorPolicy, screenPadding); + clearLocationExplicit(); + + WindowLocationAlgorithm algorithm = WindowRelocator.newRelocationAlgorithm( + screenAnchor, stageAnchor, anchorPolicy,screenPadding); if (isShowing()) { - request.accept(this); + applyLocationAlgorithm(algorithm); } else { - this.relocationRequest = request; + this.locationAlgorithm = algorithm; } } - @Override - public void centerOnScreen() { - relocationRequest = null; // cancel previous relocation request - super.centerOnScreen(); - } - /** * Closes this {@code Stage}. * This call is equivalent to {@code hide()}. @@ -1405,9 +1396,15 @@ private void setPrefHeaderButtonHeight(double height) { } @Override - final Consumer getBoundsConfigurator() { - return relocationRequest; + final void clearLocationExplicit() { + locationAlgorithm = null; + super.clearLocationExplicit(); + } + + @Override + final WindowLocationAlgorithm getLocationAlgorithm() { + return locationAlgorithm; } - private Consumer relocationRequest; + private WindowLocationAlgorithm locationAlgorithm; } diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java index 07327cec085..916b4ab604f 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java @@ -26,7 +26,6 @@ package javafx.stage; import java.util.HashMap; -import java.util.function.Consumer; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -62,6 +61,7 @@ import com.sun.javafx.stage.WindowEventDispatcher; import com.sun.javafx.stage.WindowHelper; import com.sun.javafx.stage.WindowPeerListener; +import com.sun.javafx.stage.WindowLocationAlgorithm; import com.sun.javafx.tk.TKPulseListener; import com.sun.javafx.tk.TKScene; import com.sun.javafx.tk.TKStage; @@ -136,6 +136,11 @@ public void doVisibleChanged(Window window, boolean visible) { window.doVisibleChanged(visible); } + @Override + public Window getWindowOwner(Window window) { + return window.getWindowOwner(); + } + @Override public TKStage getPeer(Window window) { return window.getPeer(); @@ -335,6 +340,16 @@ private void adjustSize(boolean selfSizePriority) { private static final float CENTER_ON_SCREEN_X_FRACTION = 1.0f / 2; private static final float CENTER_ON_SCREEN_Y_FRACTION = 1.0f / 3; + private static final WindowLocationAlgorithm CENTER_ON_SCREEN_ALGORITHM = new WindowLocationAlgorithm() { + @Override + public ComputedLocation compute(Screen screen, double windowWidth, double windowHeight) { + Rectangle2D bounds = screen.getVisualBounds(); + double centerX = bounds.getMinX() + (bounds.getWidth() - windowWidth) * CENTER_ON_SCREEN_X_FRACTION; + double centerY = bounds.getMinY() + (bounds.getHeight() - windowHeight) * CENTER_ON_SCREEN_Y_FRACTION; + return new ComputedLocation(centerX, centerY, CENTER_ON_SCREEN_X_FRACTION, CENTER_ON_SCREEN_Y_FRACTION); + } + }; + /** * Sets x and y properties on this Window so that it is centered on the * current screen. @@ -342,22 +357,10 @@ private void adjustSize(boolean selfSizePriority) { * visual bounds of all screens. */ public void centerOnScreen() { - xExplicit = false; - yExplicit = false; + clearLocationExplicit(); + if (peer != null) { - Rectangle2D bounds = getWindowScreen().getVisualBounds(); - double centerX = - bounds.getMinX() + (bounds.getWidth() - getWidth()) - * CENTER_ON_SCREEN_X_FRACTION; - double centerY = - bounds.getMinY() + (bounds.getHeight() - getHeight()) - * CENTER_ON_SCREEN_Y_FRACTION; - - x.set(centerX); - y.set(centerY); - peerBoundsConfigurator.setLocation(centerX, centerY, - CENTER_ON_SCREEN_X_FRACTION, - CENTER_ON_SCREEN_Y_FRACTION); + applyLocationAlgorithm(CENTER_ON_SCREEN_ALGORITHM); applyBounds(); } } @@ -548,14 +551,14 @@ public final DoubleProperty renderScaleYProperty() { new ReadOnlyDoubleWrapper(this, "x", Double.NaN); public final void setX(double value) { - setXInternal(value); + setXInternal(value, 0); } public final double getX() { return x.get(); } public final ReadOnlyDoubleProperty xProperty() { return x.getReadOnlyProperty(); } - void setXInternal(double value) { + void setXInternal(double value, float gravity) { x.set(value); - peerBoundsConfigurator.setX(value, 0); + peerBoundsConfigurator.setX(value, gravity); xExplicit = true; } @@ -579,14 +582,14 @@ void setXInternal(double value) { new ReadOnlyDoubleWrapper(this, "y", Double.NaN); public final void setY(double value) { - setYInternal(value); + setYInternal(value, 0); } public final double getY() { return y.get(); } public final ReadOnlyDoubleProperty yProperty() { return y.getReadOnlyProperty(); } - void setYInternal(double value) { + void setYInternal(double value, float gravity) { y.set(value); - peerBoundsConfigurator.setY(value, 0); + peerBoundsConfigurator.setY(value, gravity); yExplicit = true; } @@ -602,6 +605,40 @@ void notifyLocationChanged(double newX, double newY) { y.set(newY); } + void clearLocationExplicit() { + xExplicit = false; + yExplicit = false; + } + + /** + * Allows subclasses to specify an algorithm that computes the window location. + */ + WindowLocationAlgorithm getLocationAlgorithm() { + return null; + } + + /** + * Applies the specified location algorithm, but does not change explicitly specified window coordinates. + */ + final void applyLocationAlgorithm(WindowLocationAlgorithm algorithm) { + if (xExplicit && yExplicit) { + return; + } + + WindowLocationAlgorithm.ComputedLocation location = + algorithm.compute(Utils.getScreenForWindow(this), getWidth(), getHeight()); + + if (!xExplicit) { + x.set(location.x()); + peerBoundsConfigurator.setX(location.x(), (float)location.xGravity()); + } + + if (!yExplicit) { + y.set(location.y()); + peerBoundsConfigurator.setY(location.y(), (float)location.yGravity()); + } + } + private boolean widthExplicit = false; /** @@ -1063,7 +1100,7 @@ public final ObjectProperty> onHiddenProperty() { } Toolkit tk = Toolkit.getToolkit(); - Consumer boundsConfigurator = getBoundsConfigurator(); + WindowLocationAlgorithm locationAlgorithm = getLocationAlgorithm(); if (peer != null) { if (newVisible) { @@ -1114,19 +1151,15 @@ public final ObjectProperty> onHiddenProperty() { getWidth(), getHeight(), -1, -1); } - if (!xExplicit && !yExplicit) { - centerOnScreen(); - } else { - peerBoundsConfigurator.setLocation(getX(), getY(), - 0, 0); - } - - // If a derived class has provided us a bounds configurator, now is the time to apply it. - if (boundsConfigurator != null) { - boundsConfigurator.accept(Window.this); - } + // Set the location of the window peer first, because it might not have a location yet. + // This is the case when a window is hidden and then shown again: we have a location, but + // the new peer doesn't know about that yet. This location might be overwritten by a + // location algorithm later if X and Y are not specified explicitly. + peerBoundsConfigurator.setLocation(x.get(), y.get(), 0, 0); - // set peer bounds before the window is shown + // If a derived class has provided us a location algorithm, now is the time to apply it. + // If we don't have a location algorithm, we use the default center-on-screen algorithm. + applyLocationAlgorithm(locationAlgorithm != null ? locationAlgorithm : CENTER_ON_SCREEN_ALGORITHM); applyBounds(); peer.setOpacity((float)getOpacity()); @@ -1166,9 +1199,10 @@ public final ObjectProperty> onHiddenProperty() { // sizeToScene() request if needed to account for the new insets. sizeToScene(); - // If the window size has changed, we need to run the bounds configurator again. - if (boundsConfigurator != null) { - boundsConfigurator.accept(Window.this); + // If the window size has changed, we need to run the location algorithm again. + if (locationAlgorithm != null) { + applyLocationAlgorithm(locationAlgorithm); + applyBounds(); } } @@ -1381,38 +1415,10 @@ final void applyBounds() { peerBoundsConfigurator.apply(); } - /** - * Allows subclasses to specify an algorithm that adjusts the window bounds - * just before the window is shown. - */ - Consumer getBoundsConfigurator() { - return null; - } - Window getWindowOwner() { return null; } - private Screen getWindowScreen() { - Window window = this; - do { - if (!Double.isNaN(window.getX()) - && !Double.isNaN(window.getY()) - && !Double.isNaN(window.getWidth()) - && !Double.isNaN(window.getHeight())) { - return Utils.getScreenForRectangle( - new Rectangle2D(window.getX(), - window.getY(), - window.getWidth(), - window.getHeight())); - } - - window = window.getWindowOwner(); - } while (window != null); - - return Screen.getPrimary(); - } - private final ReadOnlyObjectWrapper screen = new ReadOnlyObjectWrapper<>(Screen.getPrimary()); private ReadOnlyObjectProperty screenProperty() { return screen.getReadOnlyProperty(); } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 7b98929ad36..91f2e481ddf 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -555,20 +555,19 @@ public void testAddAndSetNullIcon() { @Test public void relocateNullArgumentsThrowNPE() { s.show(); - pulse(); assertNotNull(peer); - assertThrows(NullPointerException.class, () -> s.relocate(0, 0, null, AnchorPolicy.FIXED, Insets.EMPTY)); - assertThrows(NullPointerException.class, () -> s.relocate(0, 0, AnchorPoint.TOP_LEFT, null, Insets.EMPTY)); - assertThrows(NullPointerException.class, () -> s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, null)); + assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, null, AnchorPolicy.FIXED, Insets.EMPTY)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, AnchorPoint.TOP_LEFT, null, Insets.EMPTY)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, null)); } @Test public void relocateBeforeShowPositionsStageOnShow() { s.setWidth(300); s.setHeight(200); - s.relocate(100, 120, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(100, 120), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); s.show(); - pulse(); assertEquals(100, peer.x, 0.0001); assertEquals(120, peer.y, 0.0001); @@ -580,9 +579,7 @@ public void relocateAfterShowMovesStageImmediately() { s.setWidth(300); s.setHeight(200); s.show(); - pulse(); - - s.relocate(200, 220, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(200, 220), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); pulse(); assertEquals(200, peer.x, 0.0001); @@ -590,6 +587,47 @@ public void relocateAfterShowMovesStageImmediately() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + @Test + public void relocateWithProportionalScreenAnchorResolvesAgainstVisualBounds() { + // Visual bounds differ from full bounds (e.g., task bar / menu bar reserved area). + toolkit.setScreens(new ScreenConfiguration(0, 0, 800, 600, 0, 30, 800, 570, 96)); + + s.setWidth(200); + s.setHeight(100); + + // Proportional screen anchors are resolved against visual bounds when no fullscreen stage is present. + s.relocate(AnchorPoint.proportional(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + + assertEquals(0, peer.x, 0.0001); + assertEquals(30, peer.y, 0.0001); + assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); + } + + @Test + public void relocateWithProportionalScreenAnchorUsesCurrentScreen() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96), + new ScreenConfiguration(800, 0, 800, 600, 800, 40, 800, 560, 96)); + + // Ensure the stage is on screen 2 when resolving the proportional screen anchor. + s.setX(850); + s.setY(10); + s.setWidth(200); + s.setHeight(200); + + // Center stage on screen 2's visual bounds: + // screen center = (800 + 0.5*800, 40 + 0.5*560) = (1200, 320) + // stage top-left = center - (100, 100) = (1100, 220) + s.relocate(AnchorPoint.proportional(0.5, 0.5), AnchorPoint.CENTER, AnchorPolicy.FIXED, Insets.EMPTY); + s.show(); + + assertEquals(1100, peer.x, 0.0001); + assertEquals(220, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); + assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); + } + @Test public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { s.setWidth(200); @@ -598,9 +636,8 @@ public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { // If centerOnScreen were honored, we'd expect (300, 200) on 800x600. // relocate should override/cancel it. - s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); s.show(); - pulse(); assertEquals(0, peer.x, 0.0001); assertEquals(0, peer.y, 0.0001); @@ -615,9 +652,8 @@ public void relocateHonorsPaddingForEnabledEdges() { var padding = new Insets(10, 20, 30, 40); // top, right, bottom, left // Ask to place the TOP_LEFT anchor beyond the bottom-right safe area to force adjustment - s.relocate(800, 600, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(800, 600), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); s.show(); - pulse(); // Allowed top-left: x <= 800 - 20 - 200 = 580, y <= 600 - 30 - 200 = 370 assertEquals(580, peer.x, 0.0001); @@ -632,9 +668,8 @@ public void relocateNegativeInsetsDisableConstraintsPerEdge() { // Disable right and bottom constraints (negative), keep left/top enabled at 0. var padding = new Insets(0, -1, -1, 0); - s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); s.show(); - pulse(); assertEquals(790, peer.x, 0.0001); assertEquals(590, peer.y, 0.0001); @@ -648,9 +683,8 @@ public void relocateOneSidedLeftConstraintOnly() { // Enable left constraint (10), disable others var padding = new Insets(-1, -1, -1, 10); - s.relocate(0, 100, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(0, 100), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); s.show(); - pulse(); assertEquals(10, peer.x, 0.0001); assertEquals(100, peer.y, 0.0001); @@ -664,9 +698,8 @@ public void relocateFlipHorizontalFitsWithoutAdjustment() { // TOP_LEFT at (790,10) overflows to the right. // TOP_RIGHT at (790,10) => rawX=790-300=490 fits. - s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); s.show(); - pulse(); assertEquals(490, peer.x, 0.0001); assertEquals(10, peer.y, 0.0001); @@ -680,9 +713,8 @@ public void relocateAutoDiagonalBeatsAdjustOnly() { // TOP_LEFT at (790,590) overflows right and bottom. // AUTO should choose BOTTOM_RIGHT (diagonal flip) => raw=(490,390) fits with no adjustment. - s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, Insets.EMPTY); s.show(); - pulse(); assertEquals(490, peer.x, 0.0001); assertEquals(390, peer.y, 0.0001); @@ -696,9 +728,8 @@ public void relocateFlipHorizontalStillRequiresVerticalAdjustment() { // Flip horizontally resolves X, but Y still needs adjustment. // TOP_RIGHT raw = (490,590) => y clamps to 400. - s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); s.show(); - pulse(); assertEquals(490, peer.x, 0.0001); assertEquals(400, peer.y, 0.0001); @@ -712,9 +743,8 @@ public void relocateFlipVerticalStillRequiresHorizontalAdjustment() { // Flip vertically resolves Y, but X still needs adjustment. // BOTTOM_LEFT raw = (790,390) => x clamps to 500. - s.relocate(790, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_VERTICAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_VERTICAL, Insets.EMPTY); s.show(); - pulse(); assertEquals(500, peer.x, 0.0001); assertEquals(390, peer.y, 0.0001); @@ -731,9 +761,8 @@ public void relocateAutoWithRightOnlyConstraintFlipsHorizontally() { // Preferred TOP_LEFT: rawX=790 => violates right constraint (maxX=500) // AUTO should choose TOP_RIGHT: rawX = 790-300 = 490 (fits without adjustment) - s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); s.show(); - pulse(); assertEquals(490, peer.x, 0.0001); assertEquals(10, peer.y, 0.0001); @@ -751,9 +780,8 @@ public void relocateAutoWithLeftOnlyConstraintDoesNotFlipWhenFlipWouldBeWorse() // Preferred TOP_LEFT: rawX = 0 -> adjusted to 10 (cost 10) // Flipped TOP_RIGHT: rawX = 0-300 = -300 -> adjusted to 10 (cost 310) // AUTO may consider the flip, but should keep the original anchor as "better". - s.relocate(0, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(0, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); s.show(); - pulse(); assertEquals(10, peer.x, 0.0001); assertEquals(10, peer.y, 0.0001); @@ -770,9 +798,8 @@ public void relocateAutoWithBottomOnlyConstraintFlipsVertically() { // Preferred TOP_LEFT at y=590 => rawY=590 violates bottom maxY=400 // Vertical flip to BOTTOM_LEFT yields rawY=590-200=390 (fits) - s.relocate(100, 590, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(100, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); s.show(); - pulse(); assertEquals(100, peer.x, 0.0001); assertEquals(390, peer.y, 0.0001); @@ -789,9 +816,8 @@ public void relocateAutoIgnoresDisabledEdgesWhenDecidingWhetherToFlip() { // just because rawX would exceed the screen width. var constraints = new Insets(-1, -1, -1, 0); - s.relocate(790, 10, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); s.show(); - pulse(); // With only left constraint, rawX=790 is allowed (since right is disabled). assertEquals(790.0, peer.x, 0.0001); @@ -807,9 +833,8 @@ public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() s.setHeight(250); // With TOP_LEFT, choose minX/minY in non-fit scenario. - s.relocate(0, 0, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); s.show(); - pulse(); assertEquals(0, peer.x, 0.0001); assertEquals(0, peer.y, 0.0001); @@ -817,9 +842,8 @@ public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() s.hide(); s.setWidth(300); s.setHeight(250); - s.relocate(0, 0, AnchorPoint.TOP_RIGHT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_RIGHT, AnchorPolicy.FIXED, Insets.EMPTY); s.show(); - pulse(); assertEquals(-100, peer.x, 0.0001); // maxX = 200 - 300 = -100 assertEquals(0, peer.y, 0.0001); // choose minY because TOP_RIGHT has y = 0 @@ -835,12 +859,9 @@ public void relocateUsesSecondScreenBoundsForConstraints() { s.setHeight(300); // Point on screen 2, but near its bottom-right corner. - double px = 1920 + 1440 - 1; - double py = 160 + 900 - 1; - - s.relocate(px, py, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + var p = AnchorPoint.absolute(1920 + 1440 - 1, 160 + 900 - 1); + s.relocate(p, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); s.show(); - pulse(); // Clamp within screen 2: x <= 1920+1440-400 = 2960, y <= 160+900-300 = 760 assertEquals(2960, peer.x, 0.0001); @@ -856,9 +877,8 @@ public void relocateWithZeroSizeAndProportionalAnchorDoesNotProduceNaNAndConstra s.setHeight(0); // Enable all edges (Insets.EMPTY), so negative coordinate requests are constrained. - s.relocate(-10, -20, AnchorPoint.CENTER, AnchorPolicy.AUTO, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(-10, -20), AnchorPoint.CENTER, AnchorPolicy.AUTO, Insets.EMPTY); s.show(); - pulse(); // With width/height == 0, maxX == 800, and maxY == 600; raw is (-10, -20) => constrained to (0,0) assertEquals(0, peer.x, 0.0001); @@ -881,9 +901,8 @@ public void relocateWithZeroSizeAndImpossibleConstraintsChoosesSideUsingAnchorPo // y = 0.75 => choose maxY (since y > 0.5) var anchor = AnchorPoint.proportional(0.25, 0.75); - s.relocate(0, 0, anchor, AnchorPolicy.FIXED, constraints); + s.relocate(AnchorPoint.absolute(0, 0), anchor, AnchorPolicy.FIXED, constraints); s.show(); - pulse(); assertEquals(500, peer.x, 0.0001); assertEquals(200, peer.y, 0.0001); @@ -898,9 +917,8 @@ public void relocateWithZeroSizeAndAbsoluteAnchorDoesNotDivideByZero() { var constraints = new Insets(300, 400, 400, 500); var anchor = AnchorPoint.absolute(10, 10); - s.relocate(0, 0, anchor, AnchorPolicy.FIXED, constraints); + s.relocate(AnchorPoint.absolute(0, 0), anchor, AnchorPolicy.FIXED, constraints); s.show(); - pulse(); assertEquals(500, peer.x, 0.0001); // minX assertEquals(300, peer.y, 0.0001); // minY @@ -909,15 +927,14 @@ public void relocateWithZeroSizeAndAbsoluteAnchorDoesNotDivideByZero() { @ParameterizedTest @MethodSource("relocateHonorsScreenBounds_arguments") public void relocateWithFixedAnchorPolicyHonorsScreenBounds( - AnchorPoint anchor, + AnchorPoint stageAnchor, Insets screenPadding, double stageW, double stageH, double requestX, double requestY) { s.setWidth(stageW); s.setHeight(stageH); - s.relocate(requestX, requestY, anchor, AnchorPolicy.FIXED, screenPadding); + s.relocate(AnchorPoint.absolute(requestX, requestY), stageAnchor, AnchorPolicy.FIXED, screenPadding); s.show(); - pulse(); assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding); } @@ -925,15 +942,14 @@ public void relocateWithFixedAnchorPolicyHonorsScreenBounds( @ParameterizedTest @MethodSource("relocateHonorsScreenBoundsWithPadding_arguments") public void relocateWithFixedAnchorPolicyHonorsScreenBoundsWithPadding( - AnchorPoint anchor, + AnchorPoint stageAnchor, Insets screenPadding, double stageW, double stageH, double requestX, double requestY) { s.setWidth(stageW); s.setHeight(stageH); - s.relocate(requestX, requestY, anchor, AnchorPolicy.FIXED, screenPadding); + s.relocate(AnchorPoint.absolute(requestX, requestY), stageAnchor, AnchorPolicy.FIXED, screenPadding); s.show(); - pulse(); assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding); } From e5dfb0eb2c1049ca85f41f252ac738e8a57fb731 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:34:06 +0100 Subject: [PATCH 12/12] add relocate() overload that accepts Screen --- .../com/sun/javafx/stage/WindowRelocator.java | 30 ++- .../main/java/javafx/stage/AnchorPolicy.java | 9 +- .../src/main/java/javafx/stage/Stage.java | 87 +++++-- .../java/test/javafx/stage/StageTest.java | 241 +++++++++++++++--- 4 files changed, 300 insertions(+), 67 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java index ea48ec0c3fd..f4e0701ef9e 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java @@ -43,6 +43,9 @@ private WindowRelocator() {} /** * Creates a location algorithm that computes the position of the {@link Window} at the requested screen * coordinates using an {@link AnchorPoint}, {@link AnchorPolicy}, and per-edge screen constraints. + * {@code screenAnchor} is specified relative to {@code userScreen}. If {@code userScreen} is {@code null}, + * the screen anchor is specified relative to the current window screen; if the window has not been shown + * yet, it is specified relative to the primary screen. *

    * Screen edge constraints are specified by {@code screenPadding}: *

      @@ -51,32 +54,33 @@ private WindowRelocator() {} *
    * Enabled constraints reduce the usable area for placement by the given insets. */ - public static WindowLocationAlgorithm newRelocationAlgorithm(AnchorPoint screenAnchor, + public static WindowLocationAlgorithm newRelocationAlgorithm(Screen userScreen, + AnchorPoint screenAnchor, + Insets screenPadding, AnchorPoint stageAnchor, - AnchorPolicy anchorPolicy, - Insets screenPadding) { + AnchorPolicy anchorPolicy) { Objects.requireNonNull(screenAnchor, "screenAnchor cannot be null"); + Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); Objects.requireNonNull(stageAnchor, "stageAnchor cannot be null"); Objects.requireNonNull(anchorPolicy, "anchorPolicy cannot be null"); - Objects.requireNonNull(screenPadding, "screenPadding cannot be null"); return (windowScreen, windowWidth, windowHeight) -> { double screenX, screenY; double gravityX, gravityY; + Screen currentScreen = Objects.requireNonNullElse(userScreen, windowScreen); + Rectangle2D currentBounds = Utils.hasFullScreenStage(currentScreen) + ? currentScreen.getBounds() + : currentScreen.getVisualBounds(); // Compute the absolute coordinates of the screen anchor. - // If the screen anchor is specified in proportional coordinates, it refers to the complete + // If the screen anchor is specified in proportional coordinates, it is proportional to the complete // screen bounds when a full-screen stage is showing on the screen, and the visual bounds otherwise. if (screenAnchor.isProportional()) { - Rectangle2D bounds = Utils.hasFullScreenStage(windowScreen) - ? windowScreen.getBounds() - : windowScreen.getVisualBounds(); - - screenX = bounds.getMinX() + screenAnchor.getX() * bounds.getWidth(); - screenY = bounds.getMinY() + screenAnchor.getY() * bounds.getHeight(); + screenX = currentBounds.getMinX() + screenAnchor.getX() * currentBounds.getWidth(); + screenY = currentBounds.getMinY() + screenAnchor.getY() * currentBounds.getHeight(); } else { - screenX = screenAnchor.getX(); - screenY = screenAnchor.getY(); + screenX = currentBounds.getMinX() + screenAnchor.getX(); + screenY = currentBounds.getMinY() + screenAnchor.getY(); } // The absolute screen anchor might be on a different screen than the current window, so we diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java index 3e1b4638311..f4a96d0bb1c 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java @@ -26,16 +26,15 @@ package javafx.stage; import javafx.geometry.AnchorPoint; -import javafx.geometry.Insets; /** * Specifies how a window repositioning operation may adjust an anchor point when the preferred anchor * would place the window outside the usable screen area. *

    - * The stage anchor passed to {@link Stage#relocate(AnchorPoint, AnchorPoint, AnchorPolicy, Insets)} or - * specified by {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point - * on the window that should coincide with the requested screen coordinates. When the preferred anchor would - * place the window outside the usable screen area (as defined by the screen bounds and any configured insets), + * The stage anchor passed to {@link Stage#relocate(AnchorPoint, AnchorPoint)} or specified by + * {@link PopupWindow#anchorLocationProperty() PopupWindow.anchorLocation} identifies the point on the + * window that should coincide with the requested screen coordinates. When the preferred anchor would place + * the window outside the usable screen area (as defined by the screen bounds and any configured insets), * an {@code AnchorPolicy} can be used to select an alternative anchor before applying any final position * adjustment. * diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 1fd906c23e1..8aa4b05be48 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -1224,25 +1225,26 @@ public void toBack() { * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then *

      - *
    1. any previous call to {@link #centerOnScreen()} is disregarded, + *
    2. any previous call to {@code relocate(...)} or {@link #centerOnScreen()} is discarded, *
    3. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated * immediately; instead, they are updated after the stage is shown. *
    - * Calling this method is equivalent to calling - * {@code relocate(screenAnchor, stageAnchor, AnchorPolicy.FIXED, Insets.EMPTY)}. + * Calling this method is equivalent to calling {@link #relocate(AnchorPoint, Insets, AnchorPoint, AnchorPolicy) + * relocate(screenAnchor, Insets.EMPTY, stageAnchor, AnchorPolicy.FIXED)}. * - * @param screenAnchor An anchor point in absolute or proportional screen coordinates. If the screen anchor - * is {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first - * resolved against the {@linkplain Screen#getVisualBounds() visual bounds} of the screen - * that currently contains this stage; if the stage does not have a location yet, the - * primary screen is used. If a full-screen stage is showing on the screen, the screen - * anchor is resolved against its complete {@linkplain Screen#getBounds() bounds}. + * @param screenAnchor An anchor point in absolute or proportional coordinates relative to the screen that + * currently contains this stage (current screen); if the stage does not have a location + * yet, the primary screen is implied. If the screen anchor is + * {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first resolved + * against the {@linkplain Screen#getVisualBounds() visual bounds} of the current screen; + * if a full-screen stage is showing on the current screen, the screen anchor is resolved + * against its complete {@linkplain Screen#getBounds() bounds}. * @param stageAnchor An anchor point in absolute or proportional stage coordinates. * @throws NullPointerException if any of the parameters is {@code null} * @since 26 */ public final void relocate(AnchorPoint screenAnchor, AnchorPoint stageAnchor) { - relocate(screenAnchor, stageAnchor, AnchorPolicy.FIXED, Insets.EMPTY); + relocateImpl(null, screenAnchor, Insets.EMPTY, stageAnchor, AnchorPolicy.FIXED); } /** @@ -1252,38 +1254,83 @@ public final void relocate(AnchorPoint screenAnchor, AnchorPoint stageAnchor) { * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. * If called before the stage is shown, then *
      - *
    1. any previous call to {@link #centerOnScreen()} is disregarded, + *
    2. any previous call to {@code relocate(...)} or {@link #centerOnScreen()} is discarded, *
    3. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated * immediately; instead, they are updated after the stage is shown. *
    * - * @param screenAnchor An anchor point in absolute or proportional screen coordinates. If the screen anchor - * is {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first - * resolved against the {@linkplain Screen#getVisualBounds() visual bounds} of the screen - * that currently contains this stage; if the stage does not have a location yet, the - * primary screen is used. If a full-screen stage is showing on the screen, the screen - * anchor is resolved against its complete {@linkplain Screen#getBounds() bounds}. + * @param screenAnchor An anchor point in absolute or proportional coordinates relative to the screen that + * currently contains this stage (current screen); if the stage does not have a location + * yet, the primary screen is implied. If the screen anchor is + * {@linkplain AnchorPoint#proportional(double, double) proportional}, it is first resolved + * against the {@linkplain Screen#getVisualBounds() visual bounds} of the current screen; + * if a full-screen stage is showing on the current screen, the screen anchor is resolved + * against its complete {@linkplain Screen#getBounds() bounds}. + * @param screenPadding Defines per-edge constraints against the screen bounds. Each inset value specifies the + * minimum distance to maintain between the stage edge and the corresponding screen edge. + * A value {@code >= 0} enables the corresponding edge constraint; a negative value disables + * the constraint for that edge. Enabled constraints effectively shrink the usable screen + * area by the given insets. For example, a left inset of {@code 10} ensures the stage will + * not be placed closer than 10 pixels to the left screen edge. * @param stageAnchor An anchor point in absolute or proportional stage coordinates. * @param anchorPolicy Controls whether an alternative stage anchor may be used when the preferred anchor would * place the stage outside the usable screen area. Depending on the policy, the preferred * anchor location may be mirrored across the vertical/horizontal center line of the stage, * or an anchor might be selected automatically. If no alternative anchor yields a better * placement, the specified {@code stageAnchor} is used. + * @throws NullPointerException if any of the parameters is {@code null} + * @since 26 + */ + public final void relocate(AnchorPoint screenAnchor, Insets screenPadding, + AnchorPoint stageAnchor, AnchorPolicy anchorPolicy) { + relocateImpl(null, screenAnchor, screenPadding, stageAnchor, anchorPolicy); + } + + /** + * Positions this stage so that a point on the stage ({@code stageAnchor}) coincides with a point on + * the screen ({@code screenAnchor}), subject to the specified anchor policy and screen padding. + *

    + * This method may be called either before or after {@link #show()} or {@link #showAndWait()}. + * If called before the stage is shown, then + *

      + *
    1. any previous call to {@code relocate(...)} or {@link #centerOnScreen()} is discarded, + *
    2. the {@link #xProperty() X} and {@link #yProperty() Y} properties are not updated + * immediately; instead, they are updated after the stage is shown. + *
    + * + * @param screen The reference screen that defines the coordinate space for {@code screenAnchor}. + * @param screenAnchor An anchor point in absolute or proportional coordinates relative to {@code screen}. + * If the screen anchor is {@linkplain AnchorPoint#proportional(double, double) proportional}, + * it is first resolved against the {@linkplain Screen#getVisualBounds() visual bounds} of + * the screen; if a full-screen stage is showing on the screen, the screen anchor is resolved + * against its complete {@linkplain Screen#getBounds() bounds}. * @param screenPadding Defines per-edge constraints against the screen bounds. Each inset value specifies the * minimum distance to maintain between the stage edge and the corresponding screen edge. * A value {@code >= 0} enables the corresponding edge constraint; a negative value disables * the constraint for that edge. Enabled constraints effectively shrink the usable screen * area by the given insets. For example, a left inset of {@code 10} ensures the stage will * not be placed closer than 10 pixels to the left screen edge. + * @param stageAnchor An anchor point in absolute or proportional stage coordinates. + * @param anchorPolicy Controls whether an alternative stage anchor may be used when the preferred anchor would + * place the stage outside the usable screen area. Depending on the policy, the preferred + * anchor location may be mirrored across the vertical/horizontal center line of the stage, + * or an anchor might be selected automatically. If no alternative anchor yields a better + * placement, the specified {@code stageAnchor} is used. * @throws NullPointerException if any of the parameters is {@code null} * @since 26 */ - public final void relocate(AnchorPoint screenAnchor, AnchorPoint stageAnchor, - AnchorPolicy anchorPolicy, Insets screenPadding) { + public final void relocate(Screen screen, AnchorPoint screenAnchor, Insets screenPadding, + AnchorPoint stageAnchor, AnchorPolicy anchorPolicy) { + Objects.requireNonNull(screen, "screen cannot be null"); + relocateImpl(screen, screenAnchor, screenPadding, stageAnchor, anchorPolicy); + } + + private void relocateImpl(Screen screen, AnchorPoint screenAnchor, Insets screenPadding, + AnchorPoint stageAnchor, AnchorPolicy anchorPolicy) { clearLocationExplicit(); WindowLocationAlgorithm algorithm = WindowRelocator.newRelocationAlgorithm( - screenAnchor, stageAnchor, anchorPolicy,screenPadding); + screen, screenAnchor, screenPadding, stageAnchor, anchorPolicy); if (isShowing()) { applyLocationAlgorithm(algorithm); diff --git a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java index 91f2e481ddf..ba4d35da1eb 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/stage/StageTest.java @@ -33,6 +33,7 @@ import javafx.scene.Group; import javafx.scene.Scene; import javafx.stage.AnchorPolicy; +import javafx.stage.Screen; import javafx.stage.Stage; import test.com.sun.javafx.pgstub.StubStage; import test.com.sun.javafx.pgstub.StubToolkit; @@ -556,17 +557,28 @@ public void testAddAndSetNullIcon() { public void relocateNullArgumentsThrowNPE() { s.show(); assertNotNull(peer); - assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY)); - assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, null, AnchorPolicy.FIXED, Insets.EMPTY)); - assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, AnchorPoint.TOP_LEFT, null, Insets.EMPTY)); - assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, null)); + assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, null)); + + assertThrows(NullPointerException.class, () -> s.relocate(null, Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, Insets.EMPTY, null, AnchorPolicy.FIXED)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, Insets.EMPTY, AnchorPoint.TOP_LEFT, null)); + assertThrows(NullPointerException.class, () -> s.relocate(AnchorPoint.TOP_LEFT, null, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED)); + + assertThrows(NullPointerException.class, () -> s.relocate(null, null, Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED)); + assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT, Insets.EMPTY, null, AnchorPolicy.FIXED)); + assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT, Insets.EMPTY, AnchorPoint.TOP_LEFT, null)); + assertThrows(NullPointerException.class, () -> s.relocate(null, AnchorPoint.TOP_LEFT, null, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED)); } + /** + * Tests that {@code relocate()} called before {@code show()} is applied when the stage is shown. + */ @Test public void relocateBeforeShowPositionsStageOnShow() { s.setWidth(300); s.setHeight(200); - s.relocate(AnchorPoint.absolute(100, 120), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(100, 120), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(100, peer.x, 0.0001); @@ -574,12 +586,15 @@ public void relocateBeforeShowPositionsStageOnShow() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@code relocate()} called after {@code show()} updates the stage position immediately. + */ @Test public void relocateAfterShowMovesStageImmediately() { s.setWidth(300); s.setHeight(200); s.show(); - s.relocate(AnchorPoint.absolute(200, 220), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(200, 220), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); pulse(); assertEquals(200, peer.x, 0.0001); @@ -587,6 +602,9 @@ public void relocateAfterShowMovesStageImmediately() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that proportional screen anchors resolve against visual bounds. + */ @Test public void relocateWithProportionalScreenAnchorResolvesAgainstVisualBounds() { // Visual bounds differ from full bounds (e.g., task bar / menu bar reserved area). @@ -596,7 +614,7 @@ public void relocateWithProportionalScreenAnchorResolvesAgainstVisualBounds() { s.setHeight(100); // Proportional screen anchors are resolved against visual bounds when no fullscreen stage is present. - s.relocate(AnchorPoint.proportional(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.proportional(0, 0), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(0, peer.x, 0.0001); @@ -604,6 +622,9 @@ public void relocateWithProportionalScreenAnchorResolvesAgainstVisualBounds() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that proportional screen anchors resolve against the stage's current screen. + */ @Test public void relocateWithProportionalScreenAnchorUsesCurrentScreen() { toolkit.setScreens( @@ -619,15 +640,122 @@ public void relocateWithProportionalScreenAnchorUsesCurrentScreen() { // Center stage on screen 2's visual bounds: // screen center = (800 + 0.5*800, 40 + 0.5*560) = (1200, 320) // stage top-left = center - (100, 100) = (1100, 220) - s.relocate(AnchorPoint.proportional(0.5, 0.5), AnchorPoint.CENTER, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.proportional(0.5, 0.5), Insets.EMPTY, AnchorPoint.CENTER, AnchorPolicy.FIXED); + s.show(); + + assertEquals(1100, peer.x, 0.0001); + assertEquals(220, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); + assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); + } + + /** + * Absolute screenAnchor is relative to the specified screen's reference rectangle. + */ + @Test + public void relocateWithScreenParameterAbsoluteAnchorIsRelativeToScreenVisualBounds() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96), + new ScreenConfiguration(800, 0, 800, 600, 800, 40, 800, 560, 96)); + + s.setWidth(100); + s.setHeight(100); + + Screen screen2 = Screen.getScreens().get(1); + + // Absolute coordinates are relative to the reference rectangle of screen2. + // Here: visual min = (800, 40), so (10, 20) => (810, 60) + s.relocate(screen2, AnchorPoint.absolute(10, 20), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); + s.show(); + + assertEquals(810, peer.x, 0.0001); + assertEquals(60, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); + assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); + } + + /** + * Proportional screenAnchor uses the specified screen even if the stage is currently on another screen. + */ + @Test + public void relocateWithScreenParameterProportionalAnchorUsesSpecifiedScreen() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96), + new ScreenConfiguration(800, 0, 800, 600, 800, 40, 800, 560, 96)); + + s.setX(10); + s.setY(10); + s.setWidth(200); + s.setHeight(200); s.show(); + Screen screen2 = Screen.getScreens().get(1); + + // Center of screen2 visual bounds: + // (800 + 0.5*800, 40 + 0.5*560) = (1200, 320) + // Stage anchor CENTER => top-left = (1200-100, 320-100) = (1100, 220) + s.relocate(screen2, AnchorPoint.proportional(0.5, 0.5), Insets.EMPTY, AnchorPoint.CENTER, AnchorPolicy.FIXED); + pulse(); + assertEquals(1100, peer.x, 0.0001); assertEquals(220, peer.y, 0.0001); assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); } + /** + * Constraints (screenPadding) are applied against the specified screen's usable bounds. + */ + @Test + public void relocateWithScreenParameterHonorsPaddingOnSpecifiedScreen() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96), + new ScreenConfiguration(800, 0, 800, 600, 800, 40, 800, 560, 96)); + + s.setWidth(300); + s.setHeight(200); + + Screen screen2 = Screen.getScreens().get(1); + Insets padding = new Insets(10, 20, 30, 40); // top, right, bottom, left + + // Request a TOP_LEFT placement beyond bottom-right; should clamp within padded usable area. + // Screen2 visual: min=(800,40), size=(800,560) + // Padded usable maxX = 800+800-20 = 1580; maxY = 40+560-30 = 570 + // Stage max top-left: x = 1580-300 = 1280; y = 570-200 = 370 + s.relocate(screen2, AnchorPoint.absolute(800, 560), padding, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); + s.show(); + + assertEquals(1280, peer.x, 0.0001); + assertEquals(370, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), padding); + assertWithinBounds(peer, toolkit.getScreens().get(1), padding); + } + + /** + * "Before show" path works with the screen overload too (deferred application). + */ + @Test + public void relocateWithScreenParameterBeforeShowPositionsStageOnShow() { + toolkit.setScreens( + new ScreenConfiguration(0, 0, 800, 600, 0, 0, 800, 600, 96), + new ScreenConfiguration(800, 0, 800, 600, 800, 40, 800, 560, 96)); + + Screen screen2 = Screen.getScreens().get(1); + + s.setWidth(120); + s.setHeight(80); + s.relocate(screen2, AnchorPoint.TOP_LEFT, Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); + s.show(); + + assertEquals(800, peer.x, 0.0001); + assertEquals(40, peer.y, 0.0001); + assertNotWithinBounds(peer, toolkit.getScreens().get(0), Insets.EMPTY); + assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); + } + + /** + * Tests that {@code relocate()} called before show overrides any prior {@code centerOnScreen()} request. + */ @Test public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { s.setWidth(200); @@ -636,7 +764,7 @@ public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { // If centerOnScreen were honored, we'd expect (300, 200) on 800x600. // relocate should override/cancel it. - s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(0, peer.x, 0.0001); @@ -644,6 +772,9 @@ public void relocateCancelsCenterOnScreenWhenCalledBeforeShow() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that enabled padding insets constrain the resulting stage position. + */ @Test public void relocateHonorsPaddingForEnabledEdges() { s.setWidth(200); @@ -652,7 +783,7 @@ public void relocateHonorsPaddingForEnabledEdges() { var padding = new Insets(10, 20, 30, 40); // top, right, bottom, left // Ask to place the TOP_LEFT anchor beyond the bottom-right safe area to force adjustment - s.relocate(AnchorPoint.absolute(800, 600), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(800, 600), padding, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); // Allowed top-left: x <= 800 - 20 - 200 = 580, y <= 600 - 30 - 200 = 370 @@ -661,6 +792,9 @@ public void relocateHonorsPaddingForEnabledEdges() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), padding); } + /** + * Tests that negative insets disable constraints for the corresponding edges. + */ @Test public void relocateNegativeInsetsDisableConstraintsPerEdge() { s.setWidth(300); @@ -668,7 +802,7 @@ public void relocateNegativeInsetsDisableConstraintsPerEdge() { // Disable right and bottom constraints (negative), keep left/top enabled at 0. var padding = new Insets(0, -1, -1, 0); - s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(790, 590), padding, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(790, peer.x, 0.0001); @@ -676,6 +810,9 @@ public void relocateNegativeInsetsDisableConstraintsPerEdge() { assertNotWithinBounds(peer, toolkit.getScreens().getFirst(), padding); } + /** + * Tests that a single enabled left-edge constraint is honored. + */ @Test public void relocateOneSidedLeftConstraintOnly() { s.setWidth(300); @@ -683,7 +820,7 @@ public void relocateOneSidedLeftConstraintOnly() { // Enable left constraint (10), disable others var padding = new Insets(-1, -1, -1, 10); - s.relocate(AnchorPoint.absolute(0, 100), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, padding); + s.relocate(AnchorPoint.absolute(0, 100), padding, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(10, peer.x, 0.0001); @@ -691,6 +828,9 @@ public void relocateOneSidedLeftConstraintOnly() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), padding); } + /** + * Tests that {@link AnchorPolicy#FLIP_HORIZONTAL} selects a horizontally flipped anchor to avoid right overflow. + */ @Test public void relocateFlipHorizontalFitsWithoutAdjustment() { s.setWidth(300); @@ -698,7 +838,7 @@ public void relocateFlipHorizontalFitsWithoutAdjustment() { // TOP_LEFT at (790,10) overflows to the right. // TOP_RIGHT at (790,10) => rawX=790-300=490 fits. - s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 10), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL); s.show(); assertEquals(490, peer.x, 0.0001); @@ -706,6 +846,10 @@ public void relocateFlipHorizontalFitsWithoutAdjustment() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#AUTO} prefers a diagonal flip when both horizontal and vertical + * constraints are violated. + */ @Test public void relocateAutoDiagonalBeatsAdjustOnly() { s.setWidth(300); @@ -713,7 +857,7 @@ public void relocateAutoDiagonalBeatsAdjustOnly() { // TOP_LEFT at (790,590) overflows right and bottom. // AUTO should choose BOTTOM_RIGHT (diagonal flip) => raw=(490,390) fits with no adjustment. - s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO); s.show(); assertEquals(490, peer.x, 0.0001); @@ -721,6 +865,9 @@ public void relocateAutoDiagonalBeatsAdjustOnly() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#FLIP_HORIZONTAL} may still require vertical clamping after flipping. + */ @Test public void relocateFlipHorizontalStillRequiresVerticalAdjustment() { s.setWidth(300); @@ -728,7 +875,7 @@ public void relocateFlipHorizontalStillRequiresVerticalAdjustment() { // Flip horizontally resolves X, but Y still needs adjustment. // TOP_RIGHT raw = (490,590) => y clamps to 400. - s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_HORIZONTAL); s.show(); assertEquals(490, peer.x, 0.0001); @@ -736,6 +883,9 @@ public void relocateFlipHorizontalStillRequiresVerticalAdjustment() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#FLIP_VERTICAL} may still require horizontal clamping after flipping. + */ @Test public void relocateFlipVerticalStillRequiresHorizontalAdjustment() { s.setWidth(300); @@ -743,7 +893,7 @@ public void relocateFlipVerticalStillRequiresHorizontalAdjustment() { // Flip vertically resolves Y, but X still needs adjustment. // BOTTOM_LEFT raw = (790,390) => x clamps to 500. - s.relocate(AnchorPoint.absolute(790, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_VERTICAL, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(790, 590), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FLIP_VERTICAL); s.show(); assertEquals(500, peer.x, 0.0001); @@ -751,7 +901,10 @@ public void relocateFlipVerticalStillRequiresHorizontalAdjustment() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } - @Test + /** + * Tests that {@link AnchorPolicy#AUTO} flips horizontally when only the right constraint is enabled and violated. + */ + @Test public void relocateAutoWithRightOnlyConstraintFlipsHorizontally() { s.setWidth(300); s.setHeight(200); @@ -761,7 +914,7 @@ public void relocateAutoWithRightOnlyConstraintFlipsHorizontally() { // Preferred TOP_LEFT: rawX=790 => violates right constraint (maxX=500) // AUTO should choose TOP_RIGHT: rawX = 790-300 = 490 (fits without adjustment) - s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(790, 10), constraints, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO); s.show(); assertEquals(490, peer.x, 0.0001); @@ -769,6 +922,9 @@ public void relocateAutoWithRightOnlyConstraintFlipsHorizontally() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#AUTO} keeps the preferred anchor when flipping would worsen the adjustment. + */ @Test public void relocateAutoWithLeftOnlyConstraintDoesNotFlipWhenFlipWouldBeWorse() { s.setWidth(300); @@ -780,7 +936,7 @@ public void relocateAutoWithLeftOnlyConstraintDoesNotFlipWhenFlipWouldBeWorse() // Preferred TOP_LEFT: rawX = 0 -> adjusted to 10 (cost 10) // Flipped TOP_RIGHT: rawX = 0-300 = -300 -> adjusted to 10 (cost 310) // AUTO may consider the flip, but should keep the original anchor as "better". - s.relocate(AnchorPoint.absolute(0, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(0, 10), constraints, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO); s.show(); assertEquals(10, peer.x, 0.0001); @@ -788,6 +944,9 @@ public void relocateAutoWithLeftOnlyConstraintDoesNotFlipWhenFlipWouldBeWorse() assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#AUTO} flips vertically when only the bottom constraint is enabled and violated. + */ @Test public void relocateAutoWithBottomOnlyConstraintFlipsVertically() { s.setWidth(300); @@ -798,7 +957,7 @@ public void relocateAutoWithBottomOnlyConstraintFlipsVertically() { // Preferred TOP_LEFT at y=590 => rawY=590 violates bottom maxY=400 // Vertical flip to BOTTOM_LEFT yields rawY=590-200=390 (fits) - s.relocate(AnchorPoint.absolute(100, 590), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(100, 590), constraints, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO); s.show(); assertEquals(100, peer.x, 0.0001); @@ -806,6 +965,9 @@ public void relocateAutoWithBottomOnlyConstraintFlipsVertically() { assertWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests that {@link AnchorPolicy#AUTO} ignores disabled edge constraints when deciding whether to flip. + */ @Test public void relocateAutoIgnoresDisabledEdgesWhenDecidingWhetherToFlip() { s.setWidth(300); @@ -816,7 +978,7 @@ public void relocateAutoIgnoresDisabledEdgesWhenDecidingWhetherToFlip() { // just because rawX would exceed the screen width. var constraints = new Insets(-1, -1, -1, 0); - s.relocate(AnchorPoint.absolute(790, 10), AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO, constraints); + s.relocate(AnchorPoint.absolute(790, 10), constraints, AnchorPoint.TOP_LEFT, AnchorPolicy.AUTO); s.show(); // With only left constraint, rawX=790 is allowed (since right is disabled). @@ -825,6 +987,9 @@ public void relocateAutoIgnoresDisabledEdgesWhenDecidingWhetherToFlip() { assertNotWithinBounds(peer, toolkit.getScreens().getFirst(), Insets.EMPTY); } + /** + * Tests side selection when the stage cannot fit in the constrained span. + */ @Test public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() { // Make a screen smaller than the stage, so maxX < minX (and maxY < minY). @@ -833,7 +998,7 @@ public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() s.setHeight(250); // With TOP_LEFT, choose minX/minY in non-fit scenario. - s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); assertEquals(0, peer.x, 0.0001); assertEquals(0, peer.y, 0.0001); @@ -842,13 +1007,16 @@ public void relocateWhenStageDoesNotFitInConstrainedSpanUsesAnchorToChooseSide() s.hide(); s.setWidth(300); s.setHeight(250); - s.relocate(AnchorPoint.absolute(0, 0), AnchorPoint.TOP_RIGHT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(0, 0), Insets.EMPTY, AnchorPoint.TOP_RIGHT, AnchorPolicy.FIXED); s.show(); assertEquals(-100, peer.x, 0.0001); // maxX = 200 - 300 = -100 assertEquals(0, peer.y, 0.0001); // choose minY because TOP_RIGHT has y = 0 } + /** + * Tests that {@code relocate()} uses the screen containing the request point to apply constraints. + */ @Test public void relocateUsesSecondScreenBoundsForConstraints() { toolkit.setScreens( @@ -860,7 +1028,7 @@ public void relocateUsesSecondScreenBoundsForConstraints() { // Point on screen 2, but near its bottom-right corner. var p = AnchorPoint.absolute(1920 + 1440 - 1, 160 + 900 - 1); - s.relocate(p, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED, Insets.EMPTY); + s.relocate(p, Insets.EMPTY, AnchorPoint.TOP_LEFT, AnchorPolicy.FIXED); s.show(); // Clamp within screen 2: x <= 1920+1440-400 = 2960, y <= 160+900-300 = 760 @@ -870,6 +1038,9 @@ public void relocateUsesSecondScreenBoundsForConstraints() { assertWithinBounds(peer, toolkit.getScreens().get(1), Insets.EMPTY); } + /** + * Tests {@code relocate()} behavior for a zero-size stage with proportional anchors (no NaN/Infinity). + */ @Test public void relocateWithZeroSizeAndProportionalAnchorDoesNotProduceNaNAndConstrainsNormally() { // Force zero size at positioning time. @@ -877,7 +1048,7 @@ public void relocateWithZeroSizeAndProportionalAnchorDoesNotProduceNaNAndConstra s.setHeight(0); // Enable all edges (Insets.EMPTY), so negative coordinate requests are constrained. - s.relocate(AnchorPoint.absolute(-10, -20), AnchorPoint.CENTER, AnchorPolicy.AUTO, Insets.EMPTY); + s.relocate(AnchorPoint.absolute(-10, -20), Insets.EMPTY, AnchorPoint.CENTER, AnchorPolicy.AUTO); s.show(); // With width/height == 0, maxX == 800, and maxY == 600; raw is (-10, -20) => constrained to (0,0) @@ -887,6 +1058,9 @@ public void relocateWithZeroSizeAndProportionalAnchorDoesNotProduceNaNAndConstra assertFalse(Double.isNaN(peer.y) || Double.isInfinite(peer.y)); } + /** + * Tests side selection when constraints define an impossible usable area for a zero-size stage. + */ @Test public void relocateWithZeroSizeAndImpossibleConstraintsChoosesSideUsingAnchorPosition() { s.setWidth(0); @@ -901,13 +1075,16 @@ public void relocateWithZeroSizeAndImpossibleConstraintsChoosesSideUsingAnchorPo // y = 0.75 => choose maxY (since y > 0.5) var anchor = AnchorPoint.proportional(0.25, 0.75); - s.relocate(AnchorPoint.absolute(0, 0), anchor, AnchorPolicy.FIXED, constraints); + s.relocate(AnchorPoint.absolute(0, 0), constraints, anchor, AnchorPolicy.FIXED); s.show(); assertEquals(500, peer.x, 0.0001); assertEquals(200, peer.y, 0.0001); } + /** + * Tests that zero-size relocation with absolute anchors does not divide by zero in fallback paths. + */ @Test public void relocateWithZeroSizeAndAbsoluteAnchorDoesNotDivideByZero() { s.setWidth(0); @@ -917,13 +1094,16 @@ public void relocateWithZeroSizeAndAbsoluteAnchorDoesNotDivideByZero() { var constraints = new Insets(300, 400, 400, 500); var anchor = AnchorPoint.absolute(10, 10); - s.relocate(AnchorPoint.absolute(0, 0), anchor, AnchorPolicy.FIXED, constraints); + s.relocate(AnchorPoint.absolute(0, 0), constraints, anchor, AnchorPolicy.FIXED); s.show(); assertEquals(500, peer.x, 0.0001); // minX assertEquals(300, peer.y, 0.0001); // minY } + /** + * Tests that {@link AnchorPolicy#FIXED} constrains the stage within screen bounds for several anchors. + */ @ParameterizedTest @MethodSource("relocateHonorsScreenBounds_arguments") public void relocateWithFixedAnchorPolicyHonorsScreenBounds( @@ -933,12 +1113,15 @@ public void relocateWithFixedAnchorPolicyHonorsScreenBounds( double requestX, double requestY) { s.setWidth(stageW); s.setHeight(stageH); - s.relocate(AnchorPoint.absolute(requestX, requestY), stageAnchor, AnchorPolicy.FIXED, screenPadding); + s.relocate(AnchorPoint.absolute(requestX, requestY), screenPadding, stageAnchor, AnchorPolicy.FIXED); s.show(); assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding); } + /** + * Tests that {@link AnchorPolicy#FIXED} constrains the stage within padded usable bounds for several anchors. + */ @ParameterizedTest @MethodSource("relocateHonorsScreenBoundsWithPadding_arguments") public void relocateWithFixedAnchorPolicyHonorsScreenBoundsWithPadding( @@ -948,7 +1131,7 @@ public void relocateWithFixedAnchorPolicyHonorsScreenBoundsWithPadding( double requestX, double requestY) { s.setWidth(stageW); s.setHeight(stageH); - s.relocate(AnchorPoint.absolute(requestX, requestY), stageAnchor, AnchorPolicy.FIXED, screenPadding); + s.relocate(AnchorPoint.absolute(requestX, requestY), screenPadding, stageAnchor, AnchorPolicy.FIXED); s.show(); assertWithinBounds(peer, toolkit.getScreens().getFirst(), screenPadding);