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 new file mode 100644 index 00000000000..f4e0701ef9e --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/WindowRelocator.java @@ -0,0 +1,327 @@ +/* + * 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; + +public final class WindowRelocator { + + 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}: + *
+ * 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
+ * For each inset value:
+ *
+ * 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
+ * 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) {
+ 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,
+ 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/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
+ * An {@code AnchorPoint} provides a {@code (x, y)} coordinate together with a flag indicating
+ * how those coordinates should be interpreted:
+ *
+ * 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;
+
+ 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 offsets from the top-left corner 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) {
+ 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;
+ }
+
+ @Override
+ public String toString() {
+ return "AnchorPoint [x = " + x + ", y = " + y + ", proportional = " + proportional + "]";
+ }
+}
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..f4a96d0bb1c
--- /dev/null
+++ b/modules/javafx.graphics/src/main/java/javafx/stage/AnchorPolicy.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+/**
+ * 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)} 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.
+ *
+ * @since 26
+ */
+public enum AnchorPolicy {
+
+ /**
+ * Always use the preferred anchor and never select an alternative 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 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 usable 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 location yields a better placement, the specified {@code anchorLocation} is used.
+ *
+ * @defaultValue {@link AnchorPolicy#FIXED}
+ * @since 26
+ */
+ private final ObjectProperty
+ * This method may be called either before or after {@link #show()} or {@link #showAndWait()}.
+ * If called before the stage is shown, then
+ *
+ * This method may be called either before or after {@link #show()} or {@link #showAndWait()}.
+ * If called before the stage is shown, then
+ *
+ * This method may be called either before or after {@link #show()} or {@link #showAndWait()}.
+ * If called before the stage is shown, then
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * @since 26
+ */
+public final class AnchorPoint {
+
+ /**
+ * Anchor at the top-left corner of the target area.
+ *
+ *
+ * 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 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 a7b68f0fa54..7c97f2994d9 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;
@@ -44,8 +45,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;
@@ -61,6 +65,7 @@
import com.sun.javafx.stage.PopupWindowPeerListener;
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;
@@ -631,6 +636,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.
+ *
+ *
+ * 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 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) {
+ relocateImpl(null, screenAnchor, Insets.EMPTY, stageAnchor, AnchorPolicy.FIXED);
+ }
+
+ /**
+ * 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.
+ *
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ * @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(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(
+ screen, screenAnchor, screenPadding, stageAnchor, anchorPolicy);
+
+ if (isShowing()) {
+ applyLocationAlgorithm(algorithm);
+ } else {
+ this.locationAlgorithm = algorithm;
+ }
+ }
+
/**
* Closes this {@code Stage}.
* This call is equivalent to {@code hide()}.
@@ -1315,4 +1441,17 @@ private void setPrefHeaderButtonHeight(double height) {
peer.setPrefHeaderButtonHeight(height);
}
}
+
+ @Override
+ final void clearLocationExplicit() {
+ locationAlgorithm = null;
+ super.clearLocationExplicit();
+ }
+
+ @Override
+ final WindowLocationAlgorithm getLocationAlgorithm() {
+ return locationAlgorithm;
+ }
+
+ 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 9f518cfe7ff..916b4ab604f 100644
--- a/modules/javafx.graphics/src/main/java/javafx/stage/Window.java
+++ b/modules/javafx.graphics/src/main/java/javafx/stage/Window.java
@@ -61,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;
@@ -135,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();
@@ -334,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.
@@ -341,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();
}
}
@@ -547,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;
}
@@ -578,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;
}
@@ -601,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;
/**
@@ -1060,7 +1098,10 @@ public final ObjectProperty