diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index bfb55f95be7..e4233f9ba8a 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -30,6 +30,9 @@ import com.sun.javafx.scene.layout.HeaderButtonBehavior; import com.sun.javafx.stage.HeaderButtonMetrics; import com.sun.javafx.stage.StageHelper; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.ObjectProperty; @@ -38,7 +41,6 @@ import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.value.ObservableValue; import javafx.css.StyleableDoubleProperty; import javafx.event.Event; import javafx.geometry.Dimension2D; @@ -65,14 +67,14 @@ * method. *

* {@code HeaderBar} is a layout container that allows applications to place scene graph nodes in three areas: - * {@link #leadingProperty() leading}, {@link #centerProperty() center}, and {@link #trailingProperty() trailing}. + * {@link #leftProperty() left}, {@link #centerProperty() center}, and {@link #rightProperty() right}. * All areas can be {@code null}. The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is * set to match the height of the platform-specific default header buttons. * *

Single header bar

* Most applications should only add a single {@code HeaderBar} to the scene graph, placed at the top of the * scene and extending its entire width. This ensures that the reported values for - * {@link #leftSystemInsetProperty() leftSystemInset} and {@link #rightSystemInsetProperty() rightSystemInset}, + * {@link #leftSystemInsetProperty(Stage) leftSystemInset} and {@link #rightSystemInsetProperty(Stage) rightSystemInset}, * which describe the area reserved for the system-provided window buttons, correctly align with the location * of the {@code HeaderBar} and are taken into account when the contents of the {@code HeaderBar} are laid out. * @@ -80,8 +82,8 @@ * Applications that use multiple header bars might need to configure the additional padding inserted into the * layout to account for the system-reserved areas. For example, when two header bars are placed next to each * other in the horizontal direction, the default configuration incorrectly adds additional padding between the - * two header bars. In this case, the {@link #leadingSystemPaddingProperty() leadingSystemPadding} and - * {@link #trailingSystemPaddingProperty() trailingSystemPadding} properties can be used to remove the padding + * two header bars. In this case, the {@link #leftSystemPaddingProperty() leftSystemPadding} and + * {@link #rightSystemPaddingProperty() rightSystemPadding} properties can be used to remove the padding * that is not needed. * *

Header button height

@@ -90,6 +92,12 @@ * This can be used to achieve a more cohesive visual appearance by having the system-provided header buttons * match the height of the client-area header bar. * + *

Color scheme

+ * The color scheme of the default header buttons is automatically adjusted to remain easily recognizable + * by inspecting the {@link Scene#fillProperty() Scene.fill} property to gauge the brightness of the user + * interface. Applications should set the scene fill to a color that matches the user interface of the header + * bar area, even if the scene fill is not visible because it is obscured by other controls. + * *

Custom header buttons

* If more control over the header buttons is desired, applications can opt out of the system-provided header * buttons by setting {@link #setPrefButtonHeight(Stage, double)} to zero and place custom header buttons in @@ -106,7 +114,7 @@ * * *

Layout constraints

- * The {@code leading} and {@code trailing} children will be resized to their preferred widths and extend the + * The {@code left} and {@code right} children will be resized to their preferred widths and extend the * height of the {@code HeaderBar}. The {@code center} child will be resized to fill the available space. * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. If a child's resizable * range prevents it from be resized to fit within its position, it will be vertically centered relative to the @@ -270,6 +278,75 @@ public static double getPrefButtonHeight(Stage stage) { return StageHelper.getPrefHeaderButtonHeight(stage); } + /** + * Describes the size of the left system-reserved inset, which is an area reserved for the iconify, maximize, + * and close window buttons. If there are no window buttons on the left side of the window, the returned area + * is an empty {@code Dimension2D}. + * + * @param stage the {@code Stage} + * @return the {@code leftSystemInset} property + */ + public static ReadOnlyObjectProperty leftSystemInsetProperty(Stage stage) { + return AttachedProperties.of(stage).leftSystemInset.getReadOnlyProperty(); + } + + /** + * Gets the value of the {@link #leftSystemInsetProperty(Stage) leftSystemInset} property. + * + * @param stage the {@code Stage} + * @return the size of the left system-reserved inset + */ + public static Dimension2D getLeftSystemInset(Stage stage) { + return AttachedProperties.of(stage).leftSystemInset.get(); + } + + /** + * Describes the size of the right system-reserved inset, which is an area reserved for the iconify, maximize, + * and close window buttons. If there are no window buttons on the right side of the window, the returned area + * is an empty {@code Dimension2D}. + * + * @param stage the {@code Stage} + * @return the {@code rightSystemInset} property + */ + public static ReadOnlyObjectProperty rightSystemInsetProperty(Stage stage) { + return AttachedProperties.of(stage).rightSystemInset.getReadOnlyProperty(); + } + + /** + * Gets the value of the {@link #rightSystemInsetProperty(Stage) rightSystemInset} property. + * + * @param stage the {@code Stage} + * @return the size of the right system-reserved inset + */ + public static Dimension2D getRightSystemInset(Stage stage) { + return AttachedProperties.of(stage).rightSystemInset.get(); + } + + /** + * The system-provided minimum recommended height for the {@code HeaderBar}, which usually corresponds + * to the height of the default header buttons. Applications can use this value as a sensible lower limit + * for the height of the {@code HeaderBar}. + *

+ * By default, {@code HeaderBar}.{@link #minHeightProperty() minHeight} is set to the value of + * {@code minSystemHeight}, unless {@code minHeight} is explicitly set by a stylesheet or application code. + * + * @param stage the {@code Stage} + * @return the {@code minSystemHeight} property + */ + public static ReadOnlyDoubleProperty minSystemHeightProperty(Stage stage) { + return AttachedProperties.of(stage).minSystemHeight.getReadOnlyProperty(); + } + + /** + * Gets the value of the {@link #minSystemHeightProperty(Stage) minSystemHeight} property. + * + * @param stage the {@code Stage} + * @return the system-provided minimum recommended height for the {@code HeaderBar} + */ + public static double getMinSystemHeight(Stage stage) { + return AttachedProperties.of(stage).minSystemHeight.get(); + } + /** * Sets the alignment for the child when contained in a {@code HeaderBar}. * If set, will override the header bar's default alignment for the child's position. @@ -314,9 +391,7 @@ public static Insets getMargin(Node child) { return (Insets)Pane.getConstraint(child, MARGIN); } - private Subscription subscription = Subscription.EMPTY; - private HeaderButtonMetrics currentMetrics; - private boolean currentFullScreen; + private Subscription subscriptions = Subscription.EMPTY; /** * Creates a new {@code HeaderBar}. @@ -328,157 +403,43 @@ public HeaderBar() { // user code changes the property value before we set it to the height of the native title bar. minHeightProperty(); - ObservableValue stage = sceneProperty() + sceneProperty() .flatMap(Scene::windowProperty) - .map(w -> w instanceof Stage s ? s : null); - - stage.flatMap(Stage::fullScreenProperty) - .orElse(false) - .subscribe(this::onFullScreenChanged); - - stage.subscribe(this::onStageChanged); + .map(w -> w instanceof Stage stage ? stage : null) + .subscribe(this::onStageChanged); } /** * Creates a new {@code HeaderBar} with the specified children. * - * @param leading the leading node, or {@code null} + * @param left the left node, or {@code null} * @param center the center node, or {@code null} - * @param trailing the trailing node, or {@code null} + * @param right the right node, or {@code null} */ - public HeaderBar(Node leading, Node center, Node trailing) { + public HeaderBar(Node left, Node center, Node right) { this(); - setLeading(leading); + setLeft(left); setCenter(center); - setTrailing(trailing); - } - - private void onStageChanged(Stage stage) { - subscription.unsubscribe(); - - if (stage != null) { - subscription = StageHelper.getHeaderButtonMetrics(stage).subscribe(this::onMetricsChanged); - } - } - - private void onMetricsChanged(HeaderButtonMetrics metrics) { - currentMetrics = metrics; - updateInsets(); - } - - private void onFullScreenChanged(boolean fullScreen) { - currentFullScreen = fullScreen; - updateInsets(); - } - - private void updateInsets() { - if (currentFullScreen || currentMetrics == null) { - leftSystemInset.set(EMPTY); - rightSystemInset.set(EMPTY); - minSystemHeight.set(0); - } else { - leftSystemInset.set(currentMetrics.leftInset()); - rightSystemInset.set(currentMetrics.rightInset()); - minSystemHeight.set(currentMetrics.minHeight()); - } + setRight(right); } /** - * Describes the size of the left system-reserved inset, which is an area reserved for the iconify, maximize, - * and close window buttons. If there are no window buttons on the left side of the window, the returned area - * is an empty {@code Dimension2D}. - *

- * Note that the left system inset refers to the left side of the window, independent of layout orientation. - */ - private final ReadOnlyObjectWrapper leftSystemInset = - new ReadOnlyObjectWrapper<>(this, "leftSystemInset", EMPTY) { - @Override - protected void invalidated() { - requestLayout(); - } - }; - - public final ReadOnlyObjectProperty leftSystemInsetProperty() { - return leftSystemInset.getReadOnlyProperty(); - } - - public final Dimension2D getLeftSystemInset() { - return leftSystemInset.get(); - } - - /** - * Describes the size of the right system-reserved inset, which is an area reserved for the iconify, maximize, - * and close window buttons. If there are no window buttons on the right side of the window, the returned area - * is an empty {@code Dimension2D}. - *

- * Note that the right system inset refers to the right side of the window, independent of layout orientation. - */ - private final ReadOnlyObjectWrapper rightSystemInset = - new ReadOnlyObjectWrapper<>(this, "rightSystemInset", EMPTY) { - @Override - protected void invalidated() { - requestLayout(); - } - }; - - public final ReadOnlyObjectProperty rightSystemInsetProperty() { - return rightSystemInset.getReadOnlyProperty(); - } - - public final Dimension2D getRightSystemInset() { - return rightSystemInset.get(); - } - - /** - * The system-provided minimum recommended height for the {@code HeaderBar}, which usually corresponds - * to the height of the default header buttons. Applications can use this value as a sensible lower limit - * for the height of the {@code HeaderBar}. - *

- * By default, {@link #minHeightProperty() minHeight} is set to the value of {@code minSystemHeight}, - * unless {@code minHeight} is explicitly set by a stylesheet or application code. - */ - private final ReadOnlyDoubleWrapper minSystemHeight = - new ReadOnlyDoubleWrapper(this, "minSystemHeight") { - @Override - protected void invalidated() { - double height = get(); - var minHeight = (StyleableDoubleProperty)minHeightProperty(); - - // Only change minHeight if it was not set by a stylesheet or application code. - if (minHeight.getStyleOrigin() == null) { - minHeight.applyStyle(null, height); - } - } - }; - - public final ReadOnlyDoubleProperty minSystemHeightProperty() { - return minSystemHeight.getReadOnlyProperty(); - } - - public final double getMinSystemHeight() { - return minSystemHeight.get(); - } - - /** - * The leading area of the {@code HeaderBar}. - *

- * The leading area corresponds to the left area in a left-to-right layout, and to the right area - * in a right-to-left layout. + * The left area of the {@code HeaderBar}. * * @defaultValue {@code null} */ - private final ObjectProperty leading = new NodeProperty("leading"); + private final ObjectProperty left = new NodeProperty("left"); - public final ObjectProperty leadingProperty() { - return leading; + public final ObjectProperty leftProperty() { + return left; } - public final Node getLeading() { - return leading.get(); + public final Node getLeft() { + return left.get(); } - public final void setLeading(Node value) { - leading.set(value); + public final void setLeft(Node value) { + left.set(value); } /** @@ -501,41 +462,38 @@ public final void setCenter(Node value) { } /** - * The trailing area of the {@code HeaderBar}. - *

- * The trailing area corresponds to the right area in a left-to-right layout, and to the left area - * in a right-to-left layout. + * The right area of the {@code HeaderBar}. * * @defaultValue {@code null} */ - private final ObjectProperty trailing = new NodeProperty("trailing"); + private final ObjectProperty right = new NodeProperty("right"); - public final ObjectProperty trailingProperty() { - return trailing; + public final ObjectProperty rightProperty() { + return right; } - public final Node getTrailing() { - return trailing.get(); + public final Node getRight() { + return right.get(); } - public final void setTrailing(Node value) { - trailing.set(value); + public final void setRight(Node value) { + right.set(value); } /** - * Specifies whether additional padding should be added to the leading side of the {@code HeaderBar}. + * Specifies whether additional padding should be added to the left side of the {@code HeaderBar}. * The size of the additional padding corresponds to the size of the system-reserved area that contains * the default header buttons (iconify, maximize, and close). If the system-reserved area contains no - * header buttons, no additional padding is added to the leading side of the {@code HeaderBar}. + * header buttons, no additional padding is added to the left side of the {@code HeaderBar}. *

* Applications that use a single {@code HeaderBar} extending the entire width of the window should * set this property to {@code true} to prevent the header buttons from overlapping the content of the * {@code HeaderBar}. * * @defaultValue {@code true} - * @see #trailingSystemPaddingProperty() trailingSystemPadding + * @see #rightSystemPaddingProperty() rightSystemPadding */ - private final BooleanProperty leadingSystemPadding = new BooleanPropertyBase(true) { + private final BooleanProperty leftSystemPadding = new BooleanPropertyBase(true) { @Override public Object getBean() { return HeaderBar.this; @@ -543,7 +501,7 @@ public Object getBean() { @Override public String getName() { - return "leadingSystemPadding"; + return "leftSystemPadding"; } @Override @@ -552,32 +510,32 @@ protected void invalidated() { } }; - public final BooleanProperty leadingSystemPaddingProperty() { - return leadingSystemPadding; + public final BooleanProperty leftSystemPaddingProperty() { + return leftSystemPadding; } - public final boolean isLeadingSystemPadding() { - return leadingSystemPadding.get(); + public final boolean isLeftSystemPadding() { + return leftSystemPadding.get(); } - public final void setLeadingSystemPadding(boolean value) { - leadingSystemPadding.set(value); + public final void setLeftSystemPadding(boolean value) { + leftSystemPadding.set(value); } /** - * Specifies whether additional padding should be added to the trailing side of the {@code HeaderBar}. + * Specifies whether additional padding should be added to the right side of the {@code HeaderBar}. * The size of the additional padding corresponds to the size of the system-reserved area that contains * the default header buttons (iconify, maximize, and close). If the system-reserved area contains no - * header buttons, no additional padding is added to the trailing side of the {@code HeaderBar}. + * header buttons, no additional padding is added to the right side of the {@code HeaderBar}. *

* Applications that use a single {@code HeaderBar} extending the entire width of the window should * set this property to {@code true} to prevent the header buttons from overlapping the content of the * {@code HeaderBar}. * * @defaultValue {@code true} - * @see #leadingSystemPaddingProperty() leadingSystemPadding + * @see #leftSystemPaddingProperty() leftSystemPadding */ - private final BooleanProperty trailingSystemPadding = new BooleanPropertyBase(true) { + private final BooleanProperty rightSystemPadding = new BooleanPropertyBase(true) { @Override public Object getBean() { return HeaderBar.this; @@ -585,7 +543,7 @@ public Object getBean() { @Override public String getName() { - return "trailingSystemPadding"; + return "rightSystemPadding"; } @Override @@ -594,61 +552,59 @@ protected void invalidated() { } }; - public final BooleanProperty trailingSystemPaddingProperty() { - return trailingSystemPadding; - } - - public final boolean isTrailingSystemPadding() { - return trailingSystemPadding.get(); - } - - public final void setTrailingSystemPadding(boolean value) { - trailingSystemPadding.set(value); + public final BooleanProperty rightSystemPaddingProperty() { + return rightSystemPadding; } - private boolean isLeftSystemPadding(NodeOrientation nodeOrientation) { - return nodeOrientation == NodeOrientation.LEFT_TO_RIGHT && isLeadingSystemPadding() - || nodeOrientation == NodeOrientation.RIGHT_TO_LEFT && isTrailingSystemPadding(); + public final boolean isRightSystemPadding() { + return rightSystemPadding.get(); } - private boolean isRightSystemPadding(NodeOrientation nodeOrientation) { - return nodeOrientation == NodeOrientation.LEFT_TO_RIGHT && isTrailingSystemPadding() - || nodeOrientation == NodeOrientation.RIGHT_TO_LEFT && isLeadingSystemPadding(); + public final void setRightSystemPadding(boolean value) { + rightSystemPadding.set(value); } @Override protected double computeMinWidth(double height) { - Node leading = getLeading(); + Node left = getLeft(); Node center = getCenter(); - Node trailing = getTrailing(); + Node right = getRight(); Insets insets = getInsets(); double leftPrefWidth; double rightPrefWidth; double centerMinWidth; - double systemPaddingWidth = 0; + double leftSystemPaddingWidth = 0; + double rightSystemPaddingWidth = 0; if (height != -1 - && (childHasContentBias(leading, Orientation.VERTICAL) || - childHasContentBias(trailing, Orientation.VERTICAL) || + && (childHasContentBias(left, Orientation.VERTICAL) || + childHasContentBias(right, Orientation.VERTICAL) || childHasContentBias(center, Orientation.VERTICAL))) { double areaHeight = Math.max(0, height); - leftPrefWidth = getAreaWidth(leading, areaHeight, false); - rightPrefWidth = getAreaWidth(trailing, areaHeight, false); + leftPrefWidth = getAreaWidth(left, areaHeight, false); + rightPrefWidth = getAreaWidth(right, areaHeight, false); centerMinWidth = getAreaWidth(center, areaHeight, true); } else { - leftPrefWidth = getAreaWidth(leading, -1, false); - rightPrefWidth = getAreaWidth(trailing, -1, false); + leftPrefWidth = getAreaWidth(left, -1, false); + rightPrefWidth = getAreaWidth(right, -1, false); centerMinWidth = getAreaWidth(center, -1, true); } - NodeOrientation nodeOrientation = getEffectiveNodeOrientation(); + Scene scene = getScene(); + Stage stage = scene != null + ? scene.getWindow() instanceof Stage s ? s : null + : null; - if (isLeftSystemPadding(nodeOrientation)) { - systemPaddingWidth += getLeftSystemInset().getWidth(); - } + if (stage != null) { + var attachedProperties = AttachedProperties.of(stage); - if (isRightSystemPadding(nodeOrientation)) { - systemPaddingWidth += getRightSystemInset().getWidth(); + if (scene.getEffectiveNodeOrientation() != getEffectiveNodeOrientation()) { + leftSystemPaddingWidth = isLeftSystemPadding() ? attachedProperties.rightSystemInset.get().getWidth() : 0; + rightSystemPaddingWidth = isRightSystemPadding() ? attachedProperties.leftSystemInset.get().getWidth() : 0; + } else { + leftSystemPaddingWidth = isLeftSystemPadding() ? attachedProperties.leftSystemInset.get().getWidth() : 0; + rightSystemPaddingWidth = isRightSystemPadding() ? attachedProperties.rightSystemInset.get().getWidth() : 0; + } } return insets.getLeft() @@ -656,91 +612,93 @@ protected double computeMinWidth(double height) { + centerMinWidth + rightPrefWidth + insets.getRight() - + systemPaddingWidth; + + leftSystemPaddingWidth + + rightSystemPaddingWidth; } @Override protected double computeMinHeight(double width) { - Node leading = getLeading(); + Node left = getLeft(); Node center = getCenter(); - Node trailing = getTrailing(); + Node right = getRight(); Insets insets = getInsets(); - double leadingMinHeight = getAreaHeight(leading, -1, true); - double trailingMinHeight = getAreaHeight(trailing, -1, true); + double leftMinHeight = getAreaHeight(left, -1, true); + double rightMinHeight = getAreaHeight(right, -1, true); double centerMinHeight; if (width != -1 && childHasContentBias(center, Orientation.HORIZONTAL)) { - double leadingPrefWidth = getAreaWidth(leading, -1, false); - double trailingPrefWidth = getAreaWidth(trailing, -1, false); - centerMinHeight = getAreaHeight(center, Math.max(0, width - leadingPrefWidth - trailingPrefWidth), true); + double leftPrefWidth = getAreaWidth(left, -1, false); + double rightPrefWidth = getAreaWidth(right, -1, false); + centerMinHeight = getAreaHeight(center, Math.max(0, width - leftPrefWidth - rightPrefWidth), true); } else { centerMinHeight = getAreaHeight(center, -1, true); } return insets.getTop() + insets.getBottom() - + Math.max(centerMinHeight, Math.max(trailingMinHeight, leadingMinHeight)); + + Math.max(centerMinHeight, Math.max(rightMinHeight, leftMinHeight)); } @Override protected double computePrefHeight(double width) { - Node leading = getLeading(); + Node left = getLeft(); Node center = getCenter(); - Node trailing = getTrailing(); + Node right = getRight(); Insets insets = getInsets(); - double leadingPrefHeight = getAreaHeight(leading, -1, false); - double trailingPrefHeight = getAreaHeight(trailing, -1, false); + double leftPrefHeight = getAreaHeight(left, -1, false); + double rightPrefHeight = getAreaHeight(right, -1, false); double centerPrefHeight; if (width != -1 && childHasContentBias(center, Orientation.HORIZONTAL)) { - double leadingPrefWidth = getAreaWidth(leading, -1, false); - double trailingPrefWidth = getAreaWidth(trailing, -1, false); - centerPrefHeight = getAreaHeight(center, Math.max(0, width - leadingPrefWidth - trailingPrefWidth), false); + double leftPrefWidth = getAreaWidth(left, -1, false); + double rightPrefWidth = getAreaWidth(right, -1, false); + centerPrefHeight = getAreaHeight(center, Math.max(0, width - leftPrefWidth - rightPrefWidth), false); } else { centerPrefHeight = getAreaHeight(center, -1, false); } return insets.getTop() + insets.getBottom() - + Math.max(centerPrefHeight, Math.max(trailingPrefHeight, leadingPrefHeight)); - } - - @Override - public boolean usesMirroring() { - return false; + + Math.max(centerPrefHeight, Math.max(rightPrefHeight, leftPrefHeight)); } @Override protected void layoutChildren() { + Node left = getLeft(); Node center = getCenter(); - Node left, right; + Node right = getRight(); Insets insets = getInsets(); - NodeOrientation nodeOrientation = getEffectiveNodeOrientation(); - boolean rtl = nodeOrientation == NodeOrientation.RIGHT_TO_LEFT; double width = Math.max(getWidth(), minWidth(-1)); double height = Math.max(getHeight(), minHeight(-1)); double leftWidth = 0; double rightWidth = 0; double insideY = insets.getTop(); double insideHeight = height - insideY - insets.getBottom(); - double insideX, insideWidth; - double leftSystemPaddingWidth = isLeftSystemPadding(nodeOrientation) ? getLeftSystemInset().getWidth() : 0; - double rightSystemPaddingWidth = isRightSystemPadding(nodeOrientation) ? getRightSystemInset().getWidth() : 0; - - if (rtl) { - left = getTrailing(); - right = getLeading(); - insideX = insets.getRight() + leftSystemPaddingWidth; - insideWidth = width - insideX - insets.getLeft() - rightSystemPaddingWidth; - } else { - left = getLeading(); - right = getTrailing(); - insideX = insets.getLeft() + leftSystemPaddingWidth; - insideWidth = width - insideX - insets.getRight() - rightSystemPaddingWidth; + double rightSystemPaddingWidth = 0; + double leftSystemPaddingWidth = 0; + + Scene scene = getScene(); + Stage stage = scene != null + ? scene.getWindow() instanceof Stage s ? s : null + : null; + + if (stage != null) { + AttachedProperties attachedProperties = AttachedProperties.of(stage); + + if (scene.getEffectiveNodeOrientation() != getEffectiveNodeOrientation()) { + leftSystemPaddingWidth = isLeftSystemPadding() ? attachedProperties.rightSystemInset.get().getWidth() : 0; + rightSystemPaddingWidth = isRightSystemPadding() ? attachedProperties.leftSystemInset.get().getWidth() : 0; + } else { + leftSystemPaddingWidth = isLeftSystemPadding() ? attachedProperties.leftSystemInset.get().getWidth() : 0; + rightSystemPaddingWidth = isRightSystemPadding() ? attachedProperties.rightSystemInset.get().getWidth() : 0; + } } + double insideX = insets.getLeft() + leftSystemPaddingWidth; + double insideWidth = width - insideX - insets.getRight() - rightSystemPaddingWidth; + if (left != null && left.isManaged()) { - Insets leftMargin = adjustMarginForRTL(getNodeMargin(left), rtl); + Insets leftMargin = getNodeMargin(left); double adjustedWidth = adjustWidthByMargin(insideWidth, leftMargin); double childWidth = resizeChild(left, adjustedWidth, false, insideHeight, leftMargin); leftWidth = snapSpaceX(leftMargin.getLeft()) + childWidth + snapSpaceX(leftMargin.getRight()); @@ -756,7 +714,7 @@ protected void layoutChildren() { } if (right != null && right.isManaged()) { - Insets rightMargin = adjustMarginForRTL(getNodeMargin(right), rtl); + Insets rightMargin = getNodeMargin(right); double adjustedWidth = adjustWidthByMargin(insideWidth - leftWidth, rightMargin); double childWidth = resizeChild(right, adjustedWidth, false, insideHeight, rightMargin); rightWidth = snapSpaceX(rightMargin.getLeft()) + childWidth + snapSpaceX(rightMargin.getRight()); @@ -772,7 +730,7 @@ protected void layoutChildren() { } if (center != null && center.isManaged()) { - Insets centerMargin = adjustMarginForRTL(getNodeMargin(center), rtl); + Insets centerMargin = getNodeMargin(center); Pos alignment = getAlignment(center); if (alignment == null || alignment.getHpos() == HPos.CENTER) { @@ -809,16 +767,6 @@ protected void layoutChildren() { } } - private Insets adjustMarginForRTL(Insets margin, boolean rtl) { - if (margin == null) { - return null; - } - - return rtl - ? new Insets(margin.getTop(), margin.getLeft(), margin.getBottom(), margin.getRight()) - : margin; - } - private boolean childHasContentBias(Node child, Orientation orientation) { if (child != null && child.isManaged()) { return child.getContentBias() == orientation; @@ -864,6 +812,26 @@ private Insets getNodeMargin(Node child) { return margin != null ? margin : Insets.EMPTY; } + private void onStageChanged(Stage stage) { + subscriptions.unsubscribe(); + + if (stage != null) { + var attachedProperties = AttachedProperties.of(stage); + + subscriptions = Subscription.combine( + attachedProperties.minSystemHeight.subscribe(height -> { + var minHeight = (StyleableDoubleProperty)minHeightProperty(); + + // Only change minHeight if it was not set by a stylesheet or application code. + if (minHeight.getStyleOrigin() == null) { + minHeight.applyStyle(null, height); + } + }), + attachedProperties.subscribeLayoutInvalidated(this::requestLayout) + ); + } + } + private final class NodeProperty extends ObjectPropertyBase { private final String name; private Node value; @@ -895,4 +863,85 @@ protected void invalidated() { } } } + + /** + * This class holds attached properties that are defined on {@code HeaderBar}, but associated + * with and stored per {@code Stage}. {@code HeaderBar} uses these properties for layout purposes, + * and also subscribes to invalidation notifications that cause {@code HeaderBar} to request a + * new layout pass. + */ + private static final class AttachedProperties { + + private final Stage stage; + private final ReadOnlyObjectWrapper leftSystemInset; + private final ReadOnlyObjectWrapper rightSystemInset; + private final ReadOnlyDoubleWrapper minSystemHeight; + private final List layoutInvalidatedListeners = new ArrayList<>(); + + private boolean currentFullScreen; + private HeaderButtonMetrics currentMetrics; + + AttachedProperties(Stage stage) { + this.stage = stage; + this.leftSystemInset = new ReadOnlyObjectWrapper<>(stage, "HeaderBar.leftSystemInset", EMPTY); + this.rightSystemInset = new ReadOnlyObjectWrapper<>(stage, "HeaderBar.rightSystemInset", EMPTY); + this.minSystemHeight = new ReadOnlyDoubleWrapper(stage, "HeaderBar.minSystemHeight"); + + StageHelper.getHeaderButtonMetrics(stage).subscribe(this::onMetricsChanged); + stage.fullScreenProperty().subscribe(this::onFullScreenChanged); + stage.sceneProperty().flatMap(Scene::effectiveNodeOrientationProperty).subscribe(this::updateInsets); + } + + public static AttachedProperties of(Stage stage) { + var instance = (AttachedProperties)Objects.requireNonNull(stage, "Stage cannot be null") + .getProperties() + .get(AttachedProperties.class); + + if (instance == null) { + instance = new AttachedProperties(stage); + stage.getProperties().put(AttachedProperties.class, instance); + } + + return instance; + } + + public Subscription subscribeLayoutInvalidated(Runnable listener) { + layoutInvalidatedListeners.add(listener); + return () -> layoutInvalidatedListeners.remove(listener); + } + + private void onMetricsChanged(HeaderButtonMetrics metrics) { + currentMetrics = metrics; + + updateInsets(stage.getScene() instanceof Scene scene + ? scene.getEffectiveNodeOrientation() + : NodeOrientation.LEFT_TO_RIGHT); + } + + private void onFullScreenChanged(boolean fullScreen) { + currentFullScreen = fullScreen; + + updateInsets(stage.getScene() instanceof Scene scene + ? scene.getEffectiveNodeOrientation() + : NodeOrientation.LEFT_TO_RIGHT); + } + + private void updateInsets(NodeOrientation orientation) { + if (currentFullScreen || currentMetrics == null) { + leftSystemInset.set(EMPTY); + rightSystemInset.set(EMPTY); + minSystemHeight.set(0); + } else if (orientation == NodeOrientation.LEFT_TO_RIGHT) { + leftSystemInset.set(currentMetrics.leftInset()); + rightSystemInset.set(currentMetrics.rightInset()); + minSystemHeight.set(currentMetrics.minHeight()); + } else { + leftSystemInset.set(currentMetrics.rightInset()); + rightSystemInset.set(currentMetrics.leftInset()); + minSystemHeight.set(currentMetrics.minHeight()); + } + + layoutInvalidatedListeners.forEach(Runnable::run); + } + } } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java index 7732f6a58b7..976912be506 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java @@ -28,6 +28,7 @@ import com.sun.javafx.scene.SceneHelper; import com.sun.javafx.tk.HeaderAreaType; import com.sun.javafx.tk.TKSceneListener; +import java.lang.reflect.Method; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.geometry.Dimension2D; @@ -40,6 +41,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -52,23 +54,45 @@ @SuppressWarnings("deprecation") public class HeaderBarTest { + Stage stage; + Scene scene; HeaderBar headerBar; @BeforeEach void setup() { headerBar = new HeaderBar(); + scene = new Scene(headerBar); + stage = new Stage(); + stage.setScene(scene); + stage.show(); + } + + @AfterEach + void teardown() { + stage.close(); + } + + T getAttachedProperty(String name) { + try { + Class propertiesClass = Class.forName(HeaderBar.class.getName() + "$AttachedProperties"); + Method method = propertiesClass.getMethod("of", Stage.class); + method.setAccessible(true); + return ReflectionUtils.getFieldValue(method.invoke(null, stage), name); + } catch (Exception ex) { + throw new RuntimeException(ex); + } } @Test void emptyHeaderBar() { - assertNull(headerBar.getLeading()); + assertNull(headerBar.getLeft()); assertNull(headerBar.getCenter()); - assertNull(headerBar.getTrailing()); + assertNull(headerBar.getRight()); } @Test void minHeight_correspondsToMinSystemHeight_ifNotSetByUser() { - DoubleProperty minSystemHeight = ReflectionUtils.getFieldValue(headerBar, "minSystemHeight"); + DoubleProperty minSystemHeight = getAttachedProperty("minSystemHeight"); minSystemHeight.set(100); assertEquals(100, headerBar.minHeight(-1)); @@ -91,11 +115,11 @@ class LayoutTest { "BOTTOM_CENTER, 10, 10, 100, 80", "BOTTOM_RIGHT, 10, 10, 100, 80" }) - void alignmentOfLeadingChildOnly_resizable(Pos pos, double x, double y, double width, double height) { + void alignmentOfLeftChildOnly_resizable(Pos pos, double x, double y, double width, double height) { var content = new MockResizable(100, 50); HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); - headerBar.setLeading(content); + headerBar.setLeft(content); headerBar.resize(1000, 100); headerBar.layout(); @@ -114,11 +138,11 @@ void alignmentOfLeadingChildOnly_resizable(Pos pos, double x, double y, double w "BOTTOM_CENTER, 10, 40, 100, 50", "BOTTOM_RIGHT, 10, 40, 100, 50" }) - void alignmentOfLeadingChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { + void alignmentOfLeftChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { var content = new Rectangle(100, 50); HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); - headerBar.setLeading(content); + headerBar.setLeft(content); headerBar.resize(1000, 100); headerBar.layout(); @@ -137,11 +161,11 @@ void alignmentOfLeadingChildOnly_notResizable(Pos pos, double x, double y, doubl "BOTTOM_CENTER, 890, 10, 100, 80", "BOTTOM_RIGHT, 890, 10, 100, 80" }) - void alignmentOfTrailingChildOnly_resizable(Pos pos, double x, double y, double width, double height) { + void alignmentOfRightChildOnly_resizable(Pos pos, double x, double y, double width, double height) { var content = new MockResizable(100, 50); HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); - headerBar.setTrailing(content); + headerBar.setRight(content); headerBar.resize(1000, 100); headerBar.layout(); @@ -160,11 +184,11 @@ void alignmentOfTrailingChildOnly_resizable(Pos pos, double x, double y, double "BOTTOM_CENTER, 890, 40, 100, 50", "BOTTOM_RIGHT, 890, 40, 100, 50" }) - void alignmentOfTrailingChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { + void alignmentOfRightChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { var content = new Rectangle(100, 50); HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); - headerBar.setTrailing(content); + headerBar.setRight(content); headerBar.resize(1000, 100); headerBar.layout(); @@ -230,16 +254,16 @@ void alignmentOfCenterChildOnly_notResizable(Pos pos, double x, double y, double "BOTTOM_CENTER, 400, 10, 200, 80", "BOTTOM_RIGHT, 640, 10, 200, 80" }) - void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild( + void alignmentOfCenterChild_resizable_withNonEmptyLeftAndRightChild( Pos pos, double x, double y, double width, double height) { - var leading = new MockResizable(50, 50); + var left = new MockResizable(50, 50); var center = new MockResizable(0, 0, 100, 50, 200, 100); - var trailing = new MockResizable(150, 50); + var right = new MockResizable(150, 50); HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); - headerBar.setLeading(leading); + headerBar.setLeft(left); headerBar.setCenter(center); - headerBar.setTrailing(trailing); + headerBar.setRight(right); headerBar.resize(1000, 100); headerBar.layout(); @@ -258,16 +282,16 @@ void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild( "BOTTOM_CENTER, 450, 40, 100, 50", "BOTTOM_RIGHT, 740, 40, 100, 50" }) - void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild( + void alignmentOfCenterChild_notResizable_withNonEmptyLeftAndRightChild( Pos pos, double x, double y, double width, double height) { - var leading = new Rectangle(50, 50); + var left = new Rectangle(50, 50); var center = new Rectangle(100, 50); - var trailing = new Rectangle(150, 50); + var right = new Rectangle(150, 50); HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); - headerBar.setLeading(leading); + headerBar.setLeft(left); headerBar.setCenter(center); - headerBar.setTrailing(trailing); + headerBar.setRight(right); headerBar.resize(1000, 100); headerBar.layout(); @@ -287,7 +311,7 @@ void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild( "BOTTOM_RIGHT, 160, 10, 680, 80" }) void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, double width, double height) { - ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + ObjectProperty leftSystemInset = getAttachedProperty("leftSystemInset"); leftSystemInset.set(new Dimension2D(100, 100)); alignmentOfCenterChildImpl(pos, 1000, 1000, x, y, width, height); } @@ -306,7 +330,7 @@ void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, dou }) void alignmentOfCenterChild_withLeftSystemInset_andMaxWidthConstraint( Pos pos, double x, double y, double width, double height) { - ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + ObjectProperty leftSystemInset = getAttachedProperty("leftSystemInset"); leftSystemInset.set(new Dimension2D(100, 100)); alignmentOfCenterChildImpl(pos, 1000, 100, x, y, width, height); } @@ -324,7 +348,7 @@ void alignmentOfCenterChild_withLeftSystemInset_andMaxWidthConstraint( "BOTTOM_RIGHT, 60, 10, 680, 80" }) void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, double width, double height) { - ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + ObjectProperty rightSystemInset = getAttachedProperty("rightSystemInset"); rightSystemInset.set(new Dimension2D(100, 100)); alignmentOfCenterChildImpl(pos, 1000, 1000, x, y, width, height); } @@ -343,7 +367,7 @@ void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, do }) void alignmentOfCenterChild_withRightSystemInset_andMaxWidthConstraint( Pos pos, double x, double y, double width, double height) { - ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + ObjectProperty rightSystemInset = getAttachedProperty("rightSystemInset"); rightSystemInset.set(new Dimension2D(100, 100)); alignmentOfCenterChildImpl(pos, 1000, 100, x, y, width, height); } @@ -356,7 +380,7 @@ void alignmentOfCenterChild_withRightSystemInset_andMaxWidthConstraint( }) void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace( Pos pos, double x, double y, double width, double height) { - ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + ObjectProperty leftSystemInset = getAttachedProperty("leftSystemInset"); leftSystemInset.set(new Dimension2D(200, 100)); alignmentOfCenterChildImpl(pos, 500, 100, x, y, width, height); } @@ -369,21 +393,21 @@ void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHor }) void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace( Pos pos, double x, double y, double width, double height) { - ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + ObjectProperty rightSystemInset = getAttachedProperty("rightSystemInset"); rightSystemInset.set(new Dimension2D(200, 100)); alignmentOfCenterChildImpl(pos, 500, 100, x, y, width, height); } private void alignmentOfCenterChildImpl(Pos pos, double headerBarWidth, double maxWidth, double x, double y, double width, double height) { - var leading = new MockResizable(50, 50); + var left = new MockResizable(50, 50); var center = new MockResizable(0, 0, 100, 50, maxWidth, 100); - var trailing = new MockResizable(150, 50); + var right = new MockResizable(150, 50); HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); - headerBar.setLeading(leading); + headerBar.setLeft(left); headerBar.setCenter(center); - headerBar.setTrailing(trailing); + headerBar.setRight(right); headerBar.resize(headerBarWidth, 100); headerBar.layout(); @@ -396,19 +420,19 @@ private void alignmentOfCenterChildImpl(Pos pos, double headerBarWidth, double m "CENTER, 10, 25, 50, 50", "BOTTOM_LEFT, 10, 40, 50, 50" }) - void alignmentOfLeadingChild_notResizable_withoutReservedArea( + void alignmentOfLeftChild_notResizable_withoutReservedArea( Pos pos, double x, double y, double width, double height) { - ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + ObjectProperty leftSystemInset = getAttachedProperty("leftSystemInset"); leftSystemInset.set(new Dimension2D(100, 100)); - var leading = new Rectangle(50, 50); - HeaderBar.setAlignment(leading, pos); - HeaderBar.setMargin(leading, new Insets(10)); - headerBar.setLeadingSystemPadding(false); - headerBar.setLeading(leading); + var left = new Rectangle(50, 50); + HeaderBar.setAlignment(left, pos); + HeaderBar.setMargin(left, new Insets(10)); + headerBar.setLeftSystemPadding(false); + headerBar.setLeft(left); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds(x, y, width, height, leading); + assertBounds(x, y, width, height, left); } @ParameterizedTest @@ -417,19 +441,19 @@ void alignmentOfLeadingChild_notResizable_withoutReservedArea( "CENTER, 940, 25, 50, 50", "BOTTOM_RIGHT, 940, 40, 50, 50" }) - void alignmentOfTrailingChild_notResizable_withoutReservedArea( + void alignmentOfRightChild_notResizable_withoutReservedArea( Pos pos, double x, double y, double width, double height) { - ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + ObjectProperty rightSystemInset = getAttachedProperty("rightSystemInset"); rightSystemInset.set(new Dimension2D(100, 100)); - var trailing = new Rectangle(50, 50); - HeaderBar.setAlignment(trailing, pos); - HeaderBar.setMargin(trailing, new Insets(10)); - headerBar.setTrailingSystemPadding(false); - headerBar.setTrailing(trailing); + var right = new Rectangle(50, 50); + HeaderBar.setAlignment(right, pos); + HeaderBar.setMargin(right, new Insets(10)); + headerBar.setRightSystemPadding(false); + headerBar.setRight(right); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds(x, y, width, height, trailing); + assertBounds(x, y, width, height, right); } private void assertBounds(double x, double y, double width, double height, Node node) { diff --git a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java index c36d366f308..0011f6a328d 100644 --- a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java +++ b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java @@ -180,11 +180,11 @@ private Parent createSimpleHeaderBarRoot(Stage stage, NodeOrientation nodeOrient switch (sizeComboBox.getValue().toLowerCase(Locale.ROOT)) { case "large" -> 80; case "medium" -> 50; - default -> headerBar.getMinSystemHeight(); + default -> HeaderBar.getMinSystemHeight(stage); }); sizeComboBox.valueProperty().subscribe(event -> updateMinHeight.run()); - headerBar.minSystemHeightProperty().subscribe(event -> updateMinHeight.run()); + HeaderBar.minSystemHeightProperty(stage).subscribe(event -> updateMinHeight.run()); var menuBar = new MenuBar( new Menu("File", null, @@ -198,9 +198,9 @@ private Parent createSimpleHeaderBarRoot(Stage stage, NodeOrientation nodeOrient new MenuItem("Copy"), new MenuItem("Paste"))); - var leadingContent = new HBox(menuBar); - HeaderBar.setDragType(leadingContent, HeaderDragType.DRAGGABLE); - headerBar.setLeading(leadingContent); + var leftContent = new HBox(menuBar); + HeaderBar.setDragType(leftContent, HeaderDragType.DRAGGABLE); + headerBar.setLeft(leftContent); if (customWindowButtons) { HeaderBar.setPrefButtonHeight(stage, 0); @@ -222,16 +222,16 @@ private Parent createSimpleHeaderBarRoot(Stage stage, NodeOrientation nodeOrient }); HBox.setMargin(adaptiveButtonHeight, new Insets(4)); - leadingContent.getChildren().add(adaptiveButtonHeight); + leftContent.getChildren().add(adaptiveButtonHeight); } - var trailingNodes = new HBox(sizeComboBox); - trailingNodes.setAlignment(Pos.CENTER); - trailingNodes.setSpacing(5); - headerBar.setTrailing(trailingNodes); + var rightNodes = new HBox(sizeComboBox); + rightNodes.setAlignment(Pos.CENTER); + rightNodes.setSpacing(5); + headerBar.setRight(rightNodes); if (customWindowButtons) { - trailingNodes.getChildren().addAll(createCustomWindowButtons()); + rightNodes.getChildren().addAll(createCustomWindowButtons()); } var borderPane = new BorderPane(); @@ -244,13 +244,13 @@ private Parent createSimpleHeaderBarRoot(Stage stage, NodeOrientation nodeOrient private Parent createSplitHeaderBarRoot(Stage stage, NodeOrientation nodeOrientation, boolean customWindowButtons) { var leftHeaderBar = new HeaderBar(); leftHeaderBar.setBackground(Background.fill(Color.VIOLET)); - leftHeaderBar.setLeading(new Button("\u2728")); + leftHeaderBar.setLeft(new Button("\u2728")); leftHeaderBar.setCenter(new TextField() {{ setPromptText("Search..."); setMaxWidth(200); }}); - leftHeaderBar.setTrailingSystemPadding(false); + leftHeaderBar.setRightSystemPadding(false); var rightHeaderBar = new HeaderBar(); rightHeaderBar.setBackground(Background.fill(Color.LIGHTSKYBLUE)); - rightHeaderBar.setLeadingSystemPadding(false); + rightHeaderBar.setLeftSystemPadding(false); var sizeComboBox = new ComboBox<>(FXCollections.observableArrayList("Small", "Medium", "Large")); sizeComboBox.getSelectionModel().select(0); @@ -259,23 +259,23 @@ private Parent createSplitHeaderBarRoot(Stage stage, NodeOrientation nodeOrienta switch (sizeComboBox.getValue().toLowerCase(Locale.ROOT)) { case "large" -> 80; case "medium" -> 50; - default -> rightHeaderBar.getMinSystemHeight(); + default -> HeaderBar.getMinSystemHeight(stage); }); sizeComboBox.valueProperty().subscribe(event -> updateMinHeight.run()); - rightHeaderBar.minSystemHeightProperty().subscribe(event -> updateMinHeight.run()); + HeaderBar.minSystemHeightProperty(stage).subscribe(event -> updateMinHeight.run()); - var trailingNodes = new HBox(sizeComboBox); - trailingNodes.setAlignment(Pos.CENTER); - trailingNodes.setSpacing(5); - rightHeaderBar.setTrailing(trailingNodes); + var rightNodes = new HBox(sizeComboBox); + rightNodes.setAlignment(Pos.CENTER); + rightNodes.setSpacing(5); + rightHeaderBar.setRight(rightNodes); if (customWindowButtons) { - trailingNodes.getChildren().addAll(createCustomWindowButtons()); + rightNodes.getChildren().addAll(createCustomWindowButtons()); HeaderBar.setPrefButtonHeight(stage, 0); } - rightHeaderBar.setTrailing(trailingNodes); + rightHeaderBar.setRight(rightNodes); var left = new BorderPane(); left.setTop(leftHeaderBar);