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 getOnClose() { + return onClose.get(); + } + + public ObjectProperty> onCloseProperty() { + return onClose; + } + + public void setOnClose(EventHandler 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",