Skip to content

Commit f87448e

Browse files
author
Michael Strauß
committed
8370446: Support dialogs with StageStyle.EXTENDED (Preview)
Reviewed-by: angorya, kcr
1 parent 8341264 commit f87448e

File tree

6 files changed

+387
-47
lines changed

6 files changed

+387
-47
lines changed

modules/javafx.controls/src/main/java/javafx/scene/control/Dialog.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import javafx.event.EventType;
4343
import javafx.scene.Node;
4444
import javafx.scene.control.ButtonBar.ButtonData;
45+
import javafx.scene.layout.HeaderBar;
4546
import javafx.stage.Modality;
4647
import javafx.stage.Stage;
4748
import javafx.stage.StageStyle;
@@ -460,10 +461,10 @@ public final Modality getModality() {
460461
}
461462

462463
/**
463-
* Specifies the style for this dialog. This must be done prior to making
464-
* the dialog visible. The style is one of: StageStyle.DECORATED,
465-
* StageStyle.UNDECORATED, StageStyle.TRANSPARENT, StageStyle.UTILITY,
466-
* or StageStyle.UNIFIED.
464+
* Specifies the style for this dialog. This must be done prior to making the dialog visible.
465+
* <p>
466+
* Note that a dialog with the {@link StageStyle#EXTENDED} style should also specify a {@link HeaderBar} with
467+
* the {@link DialogPane#setHeaderBar(HeaderBar)} method, as otherwise the dialog window will not be draggable.
467468
*
468469
* @param style the style for this dialog.
469470
*

modules/javafx.controls/src/main/java/javafx/scene/control/DialogPane.java

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -31,6 +31,8 @@
3131
import java.util.Map;
3232
import java.util.WeakHashMap;
3333

34+
import com.sun.javafx.PreviewFeature;
35+
import com.sun.javafx.css.StyleManager;
3436
import com.sun.javafx.scene.control.skin.Utils;
3537
import com.sun.javafx.scene.control.skin.resources.ControlResources;
3638
import javafx.beans.DefaultProperty;
@@ -51,22 +53,24 @@
5153
import javafx.css.StyleableObjectProperty;
5254
import javafx.css.StyleableProperty;
5355
import javafx.css.StyleableStringProperty;
56+
import javafx.css.converter.StringConverter;
5457
import javafx.event.ActionEvent;
58+
import javafx.geometry.HPos;
5559
import javafx.geometry.Pos;
60+
import javafx.geometry.VPos;
5661
import javafx.scene.AccessibleRole;
5762
import javafx.scene.Node;
5863
import javafx.scene.control.ButtonBar.ButtonData;
5964
import javafx.scene.image.Image;
6065
import javafx.scene.image.ImageView;
6166
import javafx.scene.layout.ColumnConstraints;
6267
import javafx.scene.layout.GridPane;
68+
import javafx.scene.layout.HeaderBar;
6369
import javafx.scene.layout.Pane;
6470
import javafx.scene.layout.Priority;
6571
import javafx.scene.layout.Region;
6672
import javafx.scene.layout.StackPane;
67-
68-
import com.sun.javafx.css.StyleManager;
69-
import javafx.css.converter.StringConverter;
73+
import javafx.stage.StageStyle;
7074

7175
/**
7276
* DialogPane should be considered to be the root node displayed within a
@@ -430,6 +434,79 @@ public CssMetaData<DialogPane,String> getCssMetaData() {
430434
return imageUrl;
431435
}
432436

437+
// --- header bar
438+
private ObjectProperty<HeaderBar> headerBar;
439+
440+
/**
441+
* Specifies the {@link HeaderBar} for the dialog. The {@code HeaderBar} will be placed at the
442+
* top of the dialog window, and extend the entire width of the window. This property will only
443+
* be used if the dialog window is configured with the {@link StageStyle#EXTENDED} style; it has
444+
* no effect for other styles.
445+
*
446+
* @return the {@code headerBar} property
447+
* @defaultValue {@code null}
448+
* @since 26
449+
* @deprecated This is a preview feature which may be changed or removed in a future release.
450+
*/
451+
@Deprecated(since = "26")
452+
public final ObjectProperty<HeaderBar> headerBarProperty() {
453+
if (headerBar == null) {
454+
PreviewFeature.HEADER_BAR.checkEnabled();
455+
headerBar = new SimpleObjectProperty<>(this, "headerBar") {
456+
WeakReference<HeaderBar> wref = new WeakReference<>(null);
457+
458+
@Override
459+
protected void invalidated() {
460+
HeaderBar oldValue = wref.get();
461+
if (oldValue != null) {
462+
getChildren().remove(oldValue);
463+
}
464+
465+
HeaderBar newValue = get();
466+
if (newValue != null) {
467+
getChildren().add(newValue);
468+
}
469+
470+
wref = new WeakReference<>(newValue);
471+
}
472+
};
473+
}
474+
475+
return headerBar;
476+
}
477+
478+
/**
479+
* Gets the value of the {@link #headerBarProperty() headerBar} property.
480+
*
481+
* @return the {@code HeaderBar}
482+
* @since 26
483+
* @deprecated This is a preview feature which may be changed or removed in a future release.
484+
*/
485+
@Deprecated(since = "26")
486+
public final HeaderBar getHeaderBar() {
487+
PreviewFeature.HEADER_BAR.checkEnabled();
488+
return headerBar != null ? headerBar.get() : null;
489+
}
490+
491+
/**
492+
* Sets the value of the {@link #headerBarProperty() headerBar} property.
493+
*
494+
* @param value the new value
495+
* @since 26
496+
* @deprecated This is a preview feature which may be changed or removed in a future release.
497+
*/
498+
@Deprecated(since = "26")
499+
public final void setHeaderBar(HeaderBar value) {
500+
PreviewFeature.HEADER_BAR.checkEnabled();
501+
if (headerBar != null || value != null) {
502+
headerBarProperty().set(value);
503+
}
504+
}
505+
506+
// This method skips the preview feature check for internal use and can be removed when HeaderBar is finalized.
507+
private HeaderBar getHeaderBarInternal() {
508+
return headerBar != null ? headerBar.get() : null;
509+
}
433510

434511
// --- header
435512
private final ObjectProperty<Node> header = new SimpleObjectProperty<>(null) {
@@ -878,11 +955,15 @@ protected Node createDetailsButton() {
878955
final Node content = getActualContent();
879956
final Node graphic = getActualGraphic();
880957
final Node expandableContent = getExpandableContent();
958+
final HeaderBar headerBar = getHeaderBarInternal();
881959

882960
final double graphicPrefWidth = hasHeader || graphic == null ? 0 : graphic.prefWidth(-1);
883961
final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0;
884962
final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w);
885963
final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(-1);
964+
final double headerBarPrefHeight = headerBar != null
965+
? Utils.boundedSize(headerBar.prefHeight(w), headerBar.minHeight(w), headerBar.maxHeight(w))
966+
: 0;
886967

887968
final double expandableContentPrefHeight;
888969
final double contentAreaHeight;
@@ -894,16 +975,20 @@ protected Node createDetailsButton() {
894975
// precedence goes to content and then expandable content
895976
contentAreaHeight = isExpanded() ? content.prefHeight(availableContentWidth) : 0;
896977
contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
897-
expandableContentPrefHeight = h - (headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight);
978+
expandableContentPrefHeight = h - (headerBarPrefHeight + headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight);
898979
} else {
899980
// content gets the lowest precedence
900981
expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0;
901-
contentAreaHeight = h - (headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight);
982+
contentAreaHeight = h - (headerBarPrefHeight + headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight);
902983
contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
903984
}
904985

986+
if (headerBar != null) {
987+
layoutInArea(headerBar, 0, 0, w, headerBarPrefHeight, 0, HPos.LEFT, VPos.TOP);
988+
}
989+
905990
double x = leftPadding;
906-
double y = topPadding;
991+
double y = topPadding + headerBarPrefHeight;
907992

908993
if (! hasHeader) {
909994
if (graphic != null) {
@@ -933,6 +1018,7 @@ protected Node createDetailsButton() {
9331018

9341019
/** {@inheritDoc} */
9351020
@Override protected double computeMinWidth(double height) {
1021+
double headerBarMinWidth = getHeaderBarInternal() instanceof HeaderBar hb ? hb.minWidth(height) : 0;
9361022
double headerMinWidth = hasHeader() ? getActualHeader().minWidth(height) + 10 : 0;
9371023
double contentMinWidth = getActualContent().minWidth(height);
9381024
double buttonBarMinWidth = buttonBar == null ? 0 : buttonBar.minWidth(height);
@@ -949,13 +1035,14 @@ protected Node createDetailsButton() {
9491035
Math.max(Math.max(headerMinWidth, expandableContentMinWidth), Math.max(contentMinWidth, buttonBarMinWidth)) +
9501036
snappedRightInset();
9511037

952-
return snapSizeX(minWidth);
1038+
return snapSizeX(Math.max(minWidth, headerBarMinWidth));
9531039
}
9541040

9551041
/** {@inheritDoc} */
9561042
@Override protected double computeMinHeight(double width) {
9571043
final boolean hasHeader = hasHeader();
9581044

1045+
double headerBarMinHeight = getHeaderBarInternal() instanceof HeaderBar hb ? hb.minHeight(width) : 0;
9591046
double headerMinHeight = hasHeader ? getActualHeader().minHeight(width) : 0;
9601047
double buttonBarMinHeight = buttonBar == null ? 0 : buttonBar.minHeight(width);
9611048

@@ -976,6 +1063,7 @@ protected Node createDetailsButton() {
9761063
}
9771064

9781065
double minHeight = snappedTopInset() +
1066+
headerBarMinHeight +
9791067
headerMinHeight +
9801068
Math.max(graphicMinHeight, contentMinHeight) +
9811069
expandableContentMinHeight +
@@ -991,6 +1079,8 @@ protected Node createDetailsButton() {
9911079
double contentPrefWidth = getActualContent().prefWidth(height);
9921080
double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height);
9931081
double graphicPrefWidth = getActualGraphic().prefWidth(height);
1082+
double headerBarPrefWidth = getHeaderBarInternal() instanceof HeaderBar hb
1083+
? Utils.boundedSize(hb.prefWidth(height), hb.minWidth(height), hb.maxWidth(height)) : 0;
9941084

9951085
double expandableContentPrefWidth = 0;
9961086
final Node expandableContent = getExpandableContent();
@@ -1003,7 +1093,7 @@ protected Node createDetailsButton() {
10031093
Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) +
10041094
snappedRightInset();
10051095

1006-
return snapSizeX(prefWidth);
1096+
return snapSizeX(Math.max(prefWidth, headerBarPrefWidth));
10071097
}
10081098

10091099
/** {@inheritDoc} */
@@ -1012,6 +1102,8 @@ protected Node createDetailsButton() {
10121102

10131103
double headerPrefHeight = hasHeader ? getActualHeader().prefHeight(width) : 0;
10141104
double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width);
1105+
double headerBarPrefHeight = getHeaderBarInternal() instanceof HeaderBar hb
1106+
? Utils.boundedSize(hb.prefHeight(width), hb.minHeight(width), hb.maxHeight(width)) : 0;
10151107

10161108
Node graphic = getActualGraphic();
10171109
double graphicPrefWidth = hasHeader ? 0 : graphic.prefWidth(-1);
@@ -1029,6 +1121,7 @@ protected Node createDetailsButton() {
10291121
}
10301122

10311123
double prefHeight = snappedTopInset() +
1124+
headerBarPrefHeight +
10321125
headerPrefHeight +
10331126
Math.max(graphicPrefHeight, contentPrefHeight) +
10341127
expandableContentPrefHeight +

modules/javafx.controls/src/test/java/test/javafx/scene/control/DialogPaneTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
package test.javafx.scene.control;
2727

2828
import static org.junit.jupiter.api.Assertions.*;
29+
2930
import javafx.collections.ObservableList;
3031
import javafx.css.PseudoClass;
32+
import javafx.geometry.BoundingBox;
3133
import javafx.geometry.Bounds;
3234
import javafx.geometry.Insets;
3335
import javafx.scene.Node;
@@ -37,6 +39,7 @@
3739
import javafx.scene.image.Image;
3840
import javafx.scene.image.ImageView;
3941
import javafx.scene.layout.BorderPane;
42+
import javafx.scene.layout.HeaderBar;
4043
import javafx.scene.layout.StackPane;
4144
import javafx.scene.shape.Rectangle;
4245
import javafx.scene.text.Font;
@@ -161,4 +164,27 @@ public void layoutIsRequestedWhenButtonTypesChange() {
161164
dialogPane.getButtonTypes().add(ButtonType.OK);
162165
assertTrue(dialogPane.isNeedsLayout());
163166
}
167+
168+
@Test
169+
public void headerBarIsLocatedAtTopOfDialogPane() {
170+
var headerBar = new HeaderBar();
171+
headerBar.setMinHeight(20);
172+
headerBar.setPrefHeight(20);
173+
dialogPane.setHeaderBar(headerBar);
174+
dialogPane.resize(1000, 1000);
175+
dialogPane.applyCss();
176+
dialogPane.layout();
177+
178+
assertEquals(
179+
new BoundingBox(0, 0, 1000, 20),
180+
dialogPane.lookup("HeaderBar").getLayoutBounds());
181+
182+
dialogPane.getChildren().stream()
183+
.filter(Node::isVisible)
184+
.filter(c -> c != headerBar)
185+
.forEach(child ->
186+
assertTrue(child.getLayoutY() >= headerBar.getHeight(), () ->
187+
child.getClass().getSimpleName() + " must be located below HeaderBar, layoutY = " + child.getLayoutBounds())
188+
);
189+
}
164190
}

modules/javafx.controls/src/test/java/test/javafx/scene/control/DialogTest.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -102,6 +102,17 @@ public void testDialogPrefHeight() {
102102
assertEquals(prefHeight, dialog.getDialogPane().getPrefHeight(), 0);
103103
}
104104

105+
@Test
106+
public void testInitialDialogPaneIsAttachedToScene() {
107+
class TestDialog extends Dialog<ButtonType> {
108+
TestDialog() {
109+
assertNotNull(getDialogPane().getScene());
110+
}
111+
}
112+
113+
assertDoesNotThrow(TestDialog::new);
114+
}
115+
105116
@Test
106117
public void testAddAndRemoveEventHandler() {
107118
var handler = new TestHandler();

0 commit comments

Comments
 (0)