diff --git a/base/src/main/java/atlantafx/base/controls/ModalPane.java b/base/src/main/java/atlantafx/base/controls/ModalPane.java
index 1a239e1..ecc2bf7 100755
--- a/base/src/main/java/atlantafx/base/controls/ModalPane.java
+++ b/base/src/main/java/atlantafx/base/controls/ModalPane.java
@@ -246,7 +246,7 @@ public class ModalPane extends Control {
switch (side) {
case TOP -> {
setInTransitionFactory(node -> Animations.slideInDown(node, durIn));
- setOutTransitionFactory(node -> Animations.slideOutDown(node, durOut));
+ setOutTransitionFactory(node -> Animations.slideOutUp(node, durOut));
}
case RIGHT -> {
setInTransitionFactory(node -> Animations.slideInRight(node, durIn));
@@ -254,7 +254,7 @@ public class ModalPane extends Control {
}
case BOTTOM -> {
setInTransitionFactory(node -> Animations.slideInUp(node, durIn));
- setOutTransitionFactory(node -> Animations.slideOutUp(node, durOut));
+ setOutTransitionFactory(node -> Animations.slideOutDown(node, durOut));
}
case LEFT -> {
setInTransitionFactory(node -> Animations.slideInLeft(node, durIn));
diff --git a/base/src/main/java/atlantafx/base/layout/DialogPane.java b/base/src/main/java/atlantafx/base/layout/DialogPane.java
new file mode 100644
index 0000000..098fb26
--- /dev/null
+++ b/base/src/main/java/atlantafx/base/layout/DialogPane.java
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: MIT */
+
+package atlantafx.base.layout;
+
+import atlantafx.base.controls.ModalPane;
+import java.util.Objects;
+import javafx.beans.NamedArg;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.StackPane;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The DialogPane is a specialized control or layout designed to hold the
+ * {@link ModalPane} dialog content. It includes the close button out-of-the-box
+ * and allows for the addition of arbitrary children. The DialogPane is derived
+ * from the {@link AnchorPane}, so it inherits the same API. Just be sure that
+ * you haven't removed the close button while using it.
+ */
+public class DialogPane extends AnchorPane {
+
+ protected final StackPane closeButton = new StackPane();
+ protected final StackPane closeButtonIcon = new StackPane();
+ protected final @Nullable String selector;
+ protected @Nullable ModalPane modalPane;
+
+ /**
+ * Creates an DialogPane layout.
+ */
+ public DialogPane() {
+ this((String) null);
+ }
+
+ /**
+ * Creates an DialogPane layout with the given children.
+ *
+ * @param children the initial set of children for this pane
+ */
+ public DialogPane(Node... children) {
+ this((String) null, children);
+ }
+
+ /**
+ * Creates a DialogPane layout with the given children and binds
+ * the close handler to a ModalPane via CSS selector. When user clicks
+ * on the close button, it performs a ModalPane lookup via the specified
+ * selector and calls the {@link ModalPane#hide()} method automatically.
+ *
+ * @param selector the ModalPane pane CSS selector
+ * @param children the initial set of children for this pane
+ */
+ public DialogPane(@Nullable @NamedArg("selector") String selector, Node... children) {
+ super(children);
+
+ this.selector = selector;
+ this.modalPane = null;
+
+ createLayout();
+ }
+
+ /**
+ * Creates a DialogPane layout with the given children and binds
+ * the close handler to a ModalPane. When user clicks on the close button,
+ * it calls the {@link ModalPane#hide()} method automatically.
+ *
+ * @param modalPane the ModalPane pane CSS selector
+ * @param children the initial set of children for this pane
+ */
+ public DialogPane(@Nullable ModalPane modalPane, Node... children) {
+ super(children);
+
+ this.selector = null;
+ this.modalPane = modalPane;
+
+ createLayout();
+ }
+
+ /**
+ * Adds (prepends) specified node to the DialogPane before the close button.
+ *
+ *
Otherwise, if the added node takes up the full size of the DialogPane
+ * and {@link Node#isMouseTransparent()} is false, then the close button
+ * will not receive mouse events and therefore will not be clickable.
+ *
+ * @param node the node to be added
+ */
+ public void addContent(Node node) {
+ Objects.requireNonNull(node, "Node cannot be null.");
+ getChildren().add(getChildren().indexOf(closeButton), node);
+ }
+
+ protected void createLayout() {
+ closeButton.getStyleClass().add("close-button");
+ closeButton.getChildren().setAll(closeButtonIcon);
+ closeButton.setOnMouseClicked(this::handleClose);
+
+ closeButtonIcon.getStyleClass().add("icon");
+
+ getStyleClass().add("dialog-pane");
+ getChildren().add(closeButton);
+ setCloseButtonPosition();
+ }
+
+ protected void setCloseButtonPosition() {
+ setTopAnchor(closeButton, 10d);
+ setRightAnchor(closeButton, 10d);
+ }
+
+ protected void handleClose(MouseEvent event) {
+ if (modalPane != null) {
+ modalPane.hide(clearOnClose.get());
+ } else if (selector != null && getScene() != null) {
+ if (getScene().lookup(selector) instanceof ModalPane mp) {
+ // cache modal pane so that the lookup is executed only once
+ modalPane = mp;
+ modalPane.hide(clearOnClose.get());
+ }
+ }
+
+ // call user specified close handler
+ if (onClose.get() != null) {
+ onClose.get().handle(event);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Properties //
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * The property representing the user specified close handler. Note that
+ * if you have also specified the ModalPane instance or CSS selector, this
+ * handler will be executed after the default close handler. Therefore, you
+ * can use it to perform arbitrary actions on dialog close.
+ */
+ protected final ObjectProperty> onClose =
+ new SimpleObjectProperty<>(this, "onClose");
+
+ public EventHandler super MouseEvent> getOnClose() {
+ return onClose.get();
+ }
+
+ public ObjectProperty> onCloseProperty() {
+ return onClose;
+ }
+
+ public void setOnClose(EventHandler super MouseEvent> onClose) {
+ this.onClose.set(onClose);
+ }
+
+ /**
+ * See {@link ModalPane#hide(boolean)}.
+ */
+ protected final BooleanProperty clearOnClose = new SimpleBooleanProperty(this, "clearOnClose");
+
+ public boolean isClearOnClose() {
+ return clearOnClose.get();
+ }
+
+ public BooleanProperty clearOnCloseProperty() {
+ return clearOnClose;
+ }
+
+ public void setClearOnClose(boolean clearOnClose) {
+ this.clearOnClose.set(clearOnClose);
+ }
+}
diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java
index a2c698a..b2db5db 100644
--- a/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java
+++ b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java
@@ -3,6 +3,8 @@
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ModalPane;
+import atlantafx.base.controls.Tile;
+import atlantafx.base.layout.DialogPane;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.ExampleBox;
@@ -15,9 +17,12 @@ import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
@@ -41,6 +46,9 @@ public final class ModalPanePage extends OutlinePage {
// add modal pane to the root container, which is StackPane
getChildren().addAll(modalPane, modalPaneTop, modalPaneTopmost);
+ modalPane.setId("modalPane");
+ modalPaneTop.setId("modalPaneTop");
+ modalPaneTopmost.setId("modalPaneTopmost");
// reset side and transition to reuse a single modal pane between different examples
modalPane.displayProperty().addListener((obs, old, val) -> {
@@ -62,6 +70,7 @@ public final class ModalPanePage extends OutlinePage {
addSection("Nesting", nestingExample());
addSection("Maximized", maximizedExample());
addSection("Overflowed", overflowedExample());
+ addSection("DialogPane", dialogPaneExample());
addSection("Lightbox", lightboxExample());
}
@@ -296,6 +305,52 @@ public final class ModalPanePage extends OutlinePage {
return new ExampleBox(box, new Snippet(getClass(), 6), description);
}
+ private ExampleBox dialogPaneExample() {
+ //snippet_8:start
+ // you can use a selector
+ var dialog = new DialogPane("#modalPane");
+
+ // ... or your pass a ModalPane instance directly
+ //var dialog = new DialogPane(modalPane);
+
+ // ... or you can set your own close handler
+ //dialog.setOnClose(/* whatever */);
+
+ var ta = new TextArea();
+ ta.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ VBox.setVgrow(ta, Priority.ALWAYS);
+
+ var content = new VBox(
+ 20,
+ new Tile("Example Dialog", FAKER.lorem().sentence(10)),
+ ta
+ );
+ content.setPadding(new Insets(20));
+ dialog.addContent(content);
+ AnchorPane.setTopAnchor(content, 0d);
+ AnchorPane.setRightAnchor(content, 0d);
+ AnchorPane.setBottomAnchor(content, 0d);
+ AnchorPane.setLeftAnchor(content, 0d);
+
+ var openBtn = new Button("Open Dialog");
+ openBtn.setOnAction(evt -> modalPane.show(dialog));
+ //snippet_8:end
+
+ dialog.setPrefSize(450, 450);
+ dialog.setMaxSize(450, 450);
+
+ var box = new HBox(openBtn);
+ box.setAlignment(Pos.CENTER);
+
+ var description = BBCodeParser.createFormattedText("""
+ The [i]DialogPane[/i] is a specialized control (or layout) designed to hold the \
+ [i]ModalPane[/i] dialog content. It includes the close button out-of-the-box \
+ and allows for the addition of arbitrary children."""
+ );
+
+ return new ExampleBox(box, new Snippet(getClass(), 8), description);
+ }
+
private ExampleBox lightboxExample() {
//snippet_7:start
var modalImage = new ImageView();
diff --git a/styles/src/components/_index.scss b/styles/src/components/_index.scss
index 06b4328..c415c52 100755
--- a/styles/src/components/_index.scss
+++ b/styles/src/components/_index.scss
@@ -17,6 +17,7 @@
@use "label";
@use "menu";
@use "menu-button";
+@use "modal-pane";
@use "pagination";
@use "popover";
@use "progress";
diff --git a/styles/src/components/_modal-pane.scss b/styles/src/components/_modal-pane.scss
new file mode 100644
index 0000000..5c82ec4
--- /dev/null
+++ b/styles/src/components/_modal-pane.scss
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+
+@use "../settings/icons";
+
+$color-modal-bg: rgba(0, 0, 0, 0.25) !default;
+
+$color-dialog-bg: -color-bg-default !default;
+$color-dialog-close-fg: -color-fg-default !default;
+$color-dialog-close-bg-hover: -color-bg-subtle !default;
+$close-button-radius: 100px !default;
+$close-button-padding: 0.6em !default;
+$close-button-icon-size: 0.3em !default;
+
+.modal-pane {
+ -color-modal-pane-overlay: $color-modal-bg;
+
+ >.scroll-pane>.viewport>*>.scrollable-content {
+ -fx-background-color: -color-modal-pane-overlay;
+ }
+}
+
+// DialogPane is directly related to the ModalPane
+.dialog-pane {
+ -color-dialog-pane-bg: $color-dialog-bg;
+ -color-dialog-pane-close-fg: $color-dialog-close-fg;
+ -color-dialog-pane-close-bg-hover: $color-dialog-close-bg-hover;
+
+ -fx-background-color: -color-dialog-pane-bg;
+
+ >.close-button {
+ -fx-background-radius: $close-button-radius;
+ -fx-padding: $close-button-padding;
+
+ >.icon {
+ @include icons.get("close", true);
+ -fx-background-color: -color-dialog-pane-close-fg;
+ -fx-padding: $close-button-icon-size;
+ }
+
+ &:hover {
+ -fx-background-color: -color-dialog-pane-close-bg-hover;
+ }
+ }
+
+ .tile {
+ // prevent double indentation inside dialog
+ -fx-padding: 0;
+ -fx-background-radius: 0;
+ }
+}
diff --git a/styles/src/general/_extras.scss b/styles/src/general/_extras.scss
index 2aa3f81..554bf6f 100644
--- a/styles/src/general/_extras.scss
+++ b/styles/src/general/_extras.scss
@@ -14,10 +14,6 @@ $radius in cfg.$elevation {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive);
}
-.modal-pane>.scroll-pane>.viewport>*>.scrollable-content {
- -fx-background-color: rgba(0, 0, 0, 0.25);
-}
-
///////////////////////////////////////////////////////////////////////////////
// BBCode //
///////////////////////////////////////////////////////////////////////////////
diff --git a/styles/src/settings/_icons.scss b/styles/src/settings/_icons.scss
index 5177bda..539c149 100644
--- a/styles/src/settings/_icons.scss
+++ b/styles/src/settings/_icons.scss
@@ -10,6 +10,7 @@ $material-icons: (
"check": "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z",
"chevron-left": "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z",
"chevron-right": "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z",
+ "close": "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
"expand-less": "M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z",
"expand-more": "M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z",
"minus": "M 17,13 H 7 v -2 h 10 z",