From a949420f6ae84e0810e2ab86d2e61d290aef5c7c Mon Sep 17 00:00:00 2001 From: mkpaz Date: Mon, 1 May 2023 21:32:55 +0400 Subject: [PATCH] Add modal pane --- CHANGELOG.md | 1 + .../atlantafx/base/controls/ModalPane.java | 265 +++++++++++++++++ .../base/controls/ModalPaneSkin.java | 269 +++++++++++++++++ .../java/atlantafx/base/util/Animations.java | 274 ++++++++++++++++++ .../atlantafx/sampler/layout/MainModel.java | 3 + .../page/components/ModalPanePage.java | 239 +++++++++++++++ styles/src/general/_extras.scss | 4 + 7 files changed, 1055 insertions(+) create mode 100755 base/src/main/java/atlantafx/base/controls/ModalPane.java create mode 100644 base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java create mode 100755 base/src/main/java/atlantafx/base/util/Animations.java create mode 100644 sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 318209d..87bc88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - (Base) New `MaskTextField` (and `MaskTextFormatter`) component to support masked text input. - (Base) New `PasswordTextField` component to simplify `PasswordTextFormatter` usage. - (Base) 🚀 [BBCode](https://ru.wikipedia.org/wiki/BBCode) markup support. +- (Base) New `ModalPane` to display modal dialogs on top of the current scene. - (CSS) 🚀 New MacOS-like Cupertino theme in light and dark variants. - (CSS) 🚀 New [Dracula](https://ui.draculatheme.com/) theme. - (CSS) New `TabPane` style. There are three styles supported: default, floating and classic (new one). diff --git a/base/src/main/java/atlantafx/base/controls/ModalPane.java b/base/src/main/java/atlantafx/base/controls/ModalPane.java new file mode 100755 index 0000000..b7d5cc1 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/ModalPane.java @@ -0,0 +1,265 @@ +/* SPDX-License-Identifier: MIT */ + +package atlantafx.base.controls; + +import atlantafx.base.util.Animations; +import java.util.Objects; +import java.util.function.Function; +import javafx.animation.Animation; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.geometry.Side; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.util.Duration; +import org.jetbrains.annotations.Nullable; + +/** + * A container for displaying application dialogs ot top of the current scene + * without opening a modal {@link javafx.stage.Stage}. It's a translucent (glass) pane + * that can hold arbitrary content as well as animate its appearance.

+ * + *

When {@link #displayProperty()} value is changed the modal pane modifies own + * {@link #viewOrderProperty()} value accordingly, thus moving itself on top of the + * parent container or vise versa. You can change the target view order value via the + * constructor param. This also means that one must not change modal pane {@link #viewOrderProperty()} + * property manually. + */ +public class ModalPane extends Control { + + protected static final int Z_FRONT = -10; + protected static final int Z_BACK = 10; + + public static final Duration DEFAULT_DURATION_IN = Duration.millis(200); + public static final Duration DEFAULT_DURATION_OUT = Duration.millis(100); + + private final int topViewOrder; + + /** + * See {@link #ModalPane(int)}. + */ + public ModalPane() { + this(Z_FRONT); + } + + /** + * Creates a new modal pane. + * + * @param topViewOrder the {@link #viewOrderProperty()} value to be set + * to display the modal pane on top of the parent container. + */ + public ModalPane(int topViewOrder) { + super(); + this.topViewOrder = topViewOrder; + } + + @Override + protected Skin createDefaultSkin() { + return new ModalPaneSkin(this); + } + + public int getTopViewOrder() { + return topViewOrder; + } + + /////////////////////////////////////////////////////////////////////////// + // Properties // + /////////////////////////////////////////////////////////////////////////// + + protected ObjectProperty content = new SimpleObjectProperty<>(this, "content", null); + + public Node getContent() { + return content.get(); + } + + public void setContent(Node node) { + this.content.set(node); + } + + /** + * The content node to display inside the modal pane. + */ + public ObjectProperty contentProperty() { + return content; + } + + // ~ + + protected BooleanProperty display = new SimpleBooleanProperty(this, "display", false); + + public boolean isDisplay() { + return display.get(); + } + + public void setDisplay(boolean display) { + this.display.set(display); + } + + /** + * Whether the modal pane is set to top or not. + * When changed the pane {@link #viewOrderProperty()} value will be modified accordingly. + */ + public BooleanProperty displayProperty() { + return display; + } + + // ~ + + protected ObjectProperty alignment = new SimpleObjectProperty<>(this, "alignment", Pos.CENTER); + + public Pos getAlignment() { + return alignment.get(); + } + + public void setAlignment(Pos alignment) { + this.alignment.set(alignment); + } + + /** + * Content alignment. + */ + public ObjectProperty alignmentProperty() { + return alignment; + } + + // ~ + + protected ObjectProperty> inTransitionFactory = new SimpleObjectProperty<>( + this, "inTransitionFactory", node -> Animations.zoomIn(node, DEFAULT_DURATION_IN) + ); + + public Function getInTransitionFactory() { + return inTransitionFactory.get(); + } + + public void setInTransitionFactory(Function inTransitionFactory) { + this.inTransitionFactory.set(inTransitionFactory); + } + + /** + * The factory that provides a transition to be played on content appearance, + * i.e. when {@link #displayProperty()} is set to 'true'. + */ + public ObjectProperty> inTransitionFactoryProperty() { + return inTransitionFactory; + } + + // ~ + + protected ObjectProperty> outTransitionFactory = new SimpleObjectProperty<>( + this, "outTransitionFactory", node -> Animations.zoomOut(node, DEFAULT_DURATION_OUT) + ); + + public Function getOutTransitionFactory() { + return outTransitionFactory.get(); + } + + public void setOutTransitionFactory(Function outTransitionFactory) { + this.outTransitionFactory.set(outTransitionFactory); + } + + /** + * The factory that provides a transition to be played on content disappearance, + * i.e. when {@link #displayProperty()} is set to 'false'. + */ + public ObjectProperty> outTransitionFactoryProperty() { + return outTransitionFactory; + } + + // ~ + + protected BooleanProperty persistent = new SimpleBooleanProperty(this, "persistent", false); + + public boolean getPersistent() { + return persistent.get(); + } + + public void setPersistent(boolean persistent) { + this.persistent.set(persistent); + } + + /** + * Specifies whether content should be treated as persistent or not. + * By default, modal pane exits when on ESC button or mouse click outside the contenbt are. + * This property prevents this behavior and plays bouncing animation instead. + */ + public BooleanProperty persistentProperty() { + return persistent; + } + + /////////////////////////////////////////////////////////////////////////// + // Public API // + /////////////////////////////////////////////////////////////////////////// + + /** + * A convenience method for setting the content of the modal pane content + * and triggering display state at the same time. + */ + public void show(Node node) { + // calling show method with no content specified doesn't make any sense + Objects.requireNonNull(content, "Content cannot be null."); + setContent(node); + setDisplay(true); + } + + /** + * See {@link #hide(boolean)}. + */ + public void hide() { + hide(false); + } + + /** + * A convenience method for clearing the content of the modal pane content + * and triggering display state at the same time. + */ + public void hide(boolean clear) { + setDisplay(false); + if (clear) { + setContent(null); + } + } + + /** + * Sets the predefined factory for both {@link #inTransitionFactoryProperty()} and + * {@link #outTransitionFactoryProperty()} based on content position. + */ + public void usePredefinedTransitionFactories(@Nullable Side side) { + usePredefinedTransitionFactories(side, DEFAULT_DURATION_IN, DEFAULT_DURATION_OUT); + } + + public void usePredefinedTransitionFactories(@Nullable Side side, + @Nullable Duration inDuration, + @Nullable Duration outDuration) { + Duration durIn = Objects.requireNonNullElse(inDuration, DEFAULT_DURATION_IN); + Duration durOut = Objects.requireNonNullElse(outDuration, DEFAULT_DURATION_OUT); + + if (side == null) { + setInTransitionFactory(node -> Animations.zoomIn(node, durIn)); + setOutTransitionFactory(node -> Animations.fadeOut(node, durOut)); + } else { + switch (side) { + case TOP -> { + setInTransitionFactory(node -> Animations.slideInDown(node, durIn)); + setOutTransitionFactory(node -> Animations.slideOutDown(node, durOut)); + } + case RIGHT -> { + setInTransitionFactory(node -> Animations.slideInRight(node, durIn)); + setOutTransitionFactory(node -> Animations.slideOutRight(node, durOut)); + } + case BOTTOM -> { + setInTransitionFactory(node -> Animations.slideInUp(node, durIn)); + setOutTransitionFactory(node -> Animations.slideOutUp(node, durOut)); + } + case LEFT -> { + setInTransitionFactory(node -> Animations.slideInLeft(node, durIn)); + setOutTransitionFactory(node -> Animations.slideOutLeft(node, durOut)); + } + } + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java b/base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java new file mode 100644 index 0000000..fee49ee --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java @@ -0,0 +1,269 @@ +/* SPDX-License-Identifier: MIT */ + +package atlantafx.base.controls; + +import atlantafx.base.util.Animations; +import java.util.List; +import javafx.animation.Animation; +import javafx.animation.Timeline; +import javafx.beans.value.ChangeListener; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.jetbrains.annotations.Nullable; + +public class ModalPaneSkin extends SkinBase { + + protected ModalPane control; + + protected final StackPane root; + protected final ScrollPane scrollPane; + protected final StackPane contentWrapper; + + protected final EventHandler keyHandler = createKeyHandler(); + protected final EventHandler mouseHandler = createMouseHandler(); + protected final ChangeListener animationInListener = createAnimationInListener(); + protected final ChangeListener animationOutListener = createAnimationOutListener(); + + protected @Nullable List scrollbars; + protected @Nullable Animation inTransition; + protected @Nullable Animation outTransition; + + protected ModalPaneSkin(ModalPane control) { + super(control); + + root = new StackPane(); + + contentWrapper = new StackPane(); + contentWrapper.getStyleClass().add("scrollable-content"); + contentWrapper.setAlignment(Pos.CENTER); + + scrollPane = new ScrollPane(); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setFitToHeight(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setFitToWidth(true); + scrollPane.setMaxHeight(20_000); // scroll pane won't work without height specified + scrollPane.setContent(contentWrapper); + + getChildren().add(scrollPane); + control.getStyleClass().add("modal-pane"); + doHide(); + + registerListeners(); + } + + protected void registerListeners() { + registerChangeListener(getSkinnable().contentProperty(), obs -> { + @Nullable Node content = getSkinnable().getContent(); + if (content != null) { + contentWrapper.getChildren().setAll(content); + } else { + contentWrapper.getChildren().clear(); + } + + // JavaFX defers initial layout until node is first _shown_ on the scene, + // which means that animations that use node bounds won't work. + // So, we have to call it manually to init boundsInParent beforehand. + contentWrapper.layout(); + }); + + registerChangeListener(getSkinnable().displayProperty(), obs -> { + boolean display = getSkinnable().isDisplay(); + if (display) { + show(); + } else { + hide(); + } + }); + + registerChangeListener(getSkinnable().inTransitionFactoryProperty(), obs -> { + // invalidate cached value + if (inTransition != null) { + inTransition.statusProperty().removeListener(animationInListener); + } + inTransition = null; + }); + + registerChangeListener(getSkinnable().outTransitionFactoryProperty(), obs -> { + // invalidate cached value + if (outTransition != null) { + outTransition.statusProperty().removeListener(animationOutListener); + } + outTransition = null; + }); + + contentWrapper.paddingProperty().bind(getSkinnable().paddingProperty()); + contentWrapper.alignmentProperty().bind(getSkinnable().alignmentProperty()); + + // Hide overlay by pressing ESC. + // It only works when modal pane or one of its children has focus. + scrollPane.addEventHandler(KeyEvent.KEY_PRESSED, keyHandler); + + // Hide overlay by clicking outside the content area. Don't use MOUSE_CLICKED, + // because it's the same as MOUSE_RELEASED event, thus it doesn't prevent case + // when user pressed mouse button inside the content and released outside of it. + scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler); + } + + @Override + public void dispose() { + super.dispose(); + + unregisterChangeListeners(getSkinnable().contentProperty()); + unregisterChangeListeners(getSkinnable().displayProperty()); + unregisterChangeListeners(getSkinnable().inTransitionFactoryProperty()); + unregisterChangeListeners(getSkinnable().outTransitionFactoryProperty()); + + contentWrapper.paddingProperty().unbind(); + contentWrapper.alignmentProperty().unbind(); + + scrollPane.removeEventFilter(KeyEvent.KEY_PRESSED, keyHandler); + scrollPane.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler); + } + + @SuppressWarnings("ShortCircuitBoolean") + protected boolean isClickInArea(MouseEvent e, Node area) { + return (e.getX() >= area.getLayoutX() & e.getX() <= area.getLayoutX() + area.getLayoutBounds().getWidth()) + && (e.getY() >= area.getLayoutY() & e.getY() <= area.getLayoutY() + area.getLayoutBounds().getHeight()); + } + + protected EventHandler createKeyHandler() { + return event -> { + if (event.getCode() == KeyCode.ESCAPE) { + if (getSkinnable().getPersistent()) { + createCloseBlockedAnimation().playFromStart(); + } else { + hideAndConsume(event); + } + } + }; + } + + protected EventHandler createMouseHandler() { + return event -> { + @Nullable Node content = getSkinnable().getContent(); + if (event.getButton() != MouseButton.PRIMARY) { + return; + } + + if (content == null) { + hideAndConsume(event); + return; + } + + if (isClickInArea(event, content)) { + return; + } + + if (scrollbars == null || scrollbars.isEmpty()) { + scrollbars = scrollPane.lookupAll(".scroll-bar").stream() + .filter(node -> node instanceof ScrollBar) + .map(node -> (ScrollBar) node) + .toList(); + } + + var scrollBarClick = scrollbars.stream().anyMatch(scrollBar -> isClickInArea(event, scrollBar)); + if (!scrollBarClick) { + if (getSkinnable().getPersistent()) { + createCloseBlockedAnimation().playFromStart(); + } else { + hideAndConsume(event); + } + } + }; + } + + protected ChangeListener createAnimationInListener() { + return (obs, old, val) -> { + if (val == Animation.Status.RUNNING) { + doShow(); + } + }; + } + + protected ChangeListener createAnimationOutListener() { + return (obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + doHide(); + } + }; + } + + protected Timeline createCloseBlockedAnimation() { + return Animations.zoomOut(getSkinnable().getContent(), Duration.millis(100), 0.98); + } + + protected void show() { + if (getSkinnable().getViewOrder() <= getSkinnable().getTopViewOrder()) { + return; + } + + @Nullable Node content = getSkinnable().getContent(); + if (content == null) { + doShow(); + return; + } + + if (inTransition == null && getSkinnable().getInTransitionFactory() != null) { + inTransition = getSkinnable().getInTransitionFactory().apply(content); + inTransition.statusProperty().addListener(animationInListener); + } + + if (inTransition != null) { + inTransition.playFromStart(); + } else { + doShow(); + } + } + + protected void hide() { + if (getSkinnable().getViewOrder() >= ModalPane.Z_BACK) { + return; + } + + @Nullable Node content = getSkinnable().getContent(); + if (content == null) { + doHide(); + return; + } + + if (outTransition == null && getSkinnable().getOutTransitionFactory() != null) { + outTransition = getSkinnable().getOutTransitionFactory().apply(content); + outTransition.statusProperty().addListener(animationOutListener); + } + + if (outTransition != null) { + outTransition.playFromStart(); + } else { + doHide(); + } + } + + protected void hideAndConsume(Event e) { + hide(); + e.consume(); + } + + protected void doShow() { + getSkinnable().setDisplay(true); + getSkinnable().setOpacity(1); + getSkinnable().setViewOrder(getSkinnable().getTopViewOrder()); + } + + protected void doHide() { + getSkinnable().setOpacity(0); + getSkinnable().setViewOrder(ModalPane.Z_BACK); + getSkinnable().setDisplay(false); + } +} diff --git a/base/src/main/java/atlantafx/base/util/Animations.java b/base/src/main/java/atlantafx/base/util/Animations.java new file mode 100755 index 0000000..e7f047a --- /dev/null +++ b/base/src/main/java/atlantafx/base/util/Animations.java @@ -0,0 +1,274 @@ +package atlantafx.base.util; + +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Node; +import javafx.util.Duration; + +public class Animations { + + public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + + /////////////////////////////////////////////////////////////////////////// + // FADE // + /////////////////////////////////////////////////////////////////////////// + + public static Timeline fadeIn(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.opacityProperty(), 0, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.opacityProperty(), 1, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setOpacity(1); + } + }); + + return t; + } + + public static Timeline fadeOut(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.opacityProperty(), 1, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.opacityProperty(), 0, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setOpacity(1); + } + }); + + return t; + } + + /////////////////////////////////////////////////////////////////////////// + // ZOOM // + /////////////////////////////////////////////////////////////////////////// + + public static Timeline zoomIn(Node node, Duration duration) { + return zoomIn(node, duration, 0.3); + } + + public static Timeline zoomIn(Node node, Duration duration, double startValue) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.scaleXProperty(), startValue, EASE), + new KeyValue(node.scaleYProperty(), startValue, EASE), + new KeyValue(node.scaleZProperty(), startValue, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.scaleXProperty(), 1, EASE), + new KeyValue(node.scaleYProperty(), 1, EASE), + new KeyValue(node.scaleZProperty(), 1, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setScaleX(1); + node.setScaleY(1); + node.setScaleZ(1); + } + }); + + return t; + } + + public static Timeline zoomOut(Node node, Duration duration) { + return zoomOut(node, duration, 0.3); + } + + public static Timeline zoomOut(Node node, Duration duration, double endValue) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.scaleXProperty(), 1, EASE), + new KeyValue(node.scaleYProperty(), 1, EASE), + new KeyValue(node.scaleZProperty(), 1, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.scaleXProperty(), endValue, EASE), + new KeyValue(node.scaleYProperty(), endValue, EASE), + new KeyValue(node.scaleZProperty(), endValue, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setScaleX(1); + node.setScaleY(1); + node.setScaleZ(1); + } + }); + + return t; + } + + /////////////////////////////////////////////////////////////////////////// + // SLIDE // + /////////////////////////////////////////////////////////////////////////// + + public static Timeline slideInDown(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateYProperty(), -node.getBoundsInParent().getHeight(), EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateYProperty(), 0, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateY(0); + } + }); + + return t; + } + + public static Timeline slideOutDown(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateYProperty(), 0, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateYProperty(), -node.getBoundsInParent().getWidth(), EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateY(0); + } + }); + + return t; + } + + public static Timeline slideInLeft(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateXProperty(), -node.getBoundsInParent().getWidth(), EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateXProperty(), 0, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateX(0); + } + }); + + return t; + } + + public static Timeline slideOutLeft(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateXProperty(), 0, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateXProperty(), -node.getBoundsInParent().getWidth(), EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateX(0); + } + }); + + return t; + } + + public static Timeline slideInRight(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateXProperty(), node.getBoundsInParent().getWidth(), EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateXProperty(), 0, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateX(0); + } + }); + + return t; + } + + public static Timeline slideOutRight(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateXProperty(), 0, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateXProperty(), node.getBoundsInParent().getWidth(), EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateX(0); + } + }); + + return t; + } + + public static Timeline slideInUp(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateYProperty(), node.getBoundsInParent().getHeight(), EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateYProperty(), 0, EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateY(0); + } + }); + + return t; + } + + public static Timeline slideOutUp(Node node, Duration duration) { + var t = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(node.translateYProperty(), 0, EASE) + ), + new KeyFrame(duration, + new KeyValue(node.translateYProperty(), node.getBoundsInParent().getWidth(), EASE) + ) + ); + + t.statusProperty().addListener((obs, old, val) -> { + if (val == Animation.Status.STOPPED) { + node.setTranslateY(0); + } + }); + + return t; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java index 9e38c8a..75682c9 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java @@ -23,6 +23,7 @@ import atlantafx.sampler.page.components.LabelPage; import atlantafx.sampler.page.components.ListPage; import atlantafx.sampler.page.components.MenuButtonPage; import atlantafx.sampler.page.components.MenuPage; +import atlantafx.sampler.page.components.ModalPanePage; import atlantafx.sampler.page.components.OverviewPage; import atlantafx.sampler.page.components.PaginationPage; import atlantafx.sampler.page.components.PopoverPage; @@ -190,6 +191,7 @@ public class MainModel { NAV_TREE.get(BBCodePage.class), NAV_TREE.get(BreadcrumbsPage.class), NAV_TREE.get(CustomTextFieldPage.class), + NAV_TREE.get(ModalPanePage.class), NAV_TREE.get(PopoverPage.class), NAV_TREE.get(ToggleSwitchPage.class) ); @@ -245,6 +247,7 @@ public class MainModel { MenuButtonPage.NAME, MenuButtonPage.class, "SplitMenuButton") ); + map.put(ModalPanePage.class, NavTree.Item.page(ModalPanePage.NAME, ModalPanePage.class)); map.put(PaginationPage.class, NavTree.Item.page(PaginationPage.NAME, PaginationPage.class)); map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class)); map.put(ProgressPage.class, NavTree.Item.page(ProgressPage.NAME, ProgressPage.class)); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java new file mode 100644 index 0000000..b34ab8e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java @@ -0,0 +1,239 @@ +/* SPDX-License-Identifier: MIT */ + +package atlantafx.sampler.page.components; + +import atlantafx.base.controls.ModalPane; +import atlantafx.sampler.page.AbstractPage; +import atlantafx.sampler.page.SampleBlock; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Side; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; + +public class ModalPanePage extends AbstractPage { + + public static final String NAME = "Modal Pane"; + + private final ModalPane modalPaneL1 = new ModalPane(); + private final ModalPane modalPaneL2 = new ModalPane(-15); + private final ModalPane modalPaneL3 = new ModalPane(-20); + private VBox centerDialog; + private VBox topDialog; + private VBox rightDialog; + private VBox bottomDialog; + private VBox leftDialog; + + @Override + public String getName() { + return NAME; + } + + public ModalPanePage() { + super(); + + userContent.getChildren().setAll( + modalPaneL1, + modalPaneL2, + modalPaneL3, + new SampleBlock("Playground", createPlayground()) + ); + } + + private VBox createPlayground() { + var controlPane = new GridPane(); + controlPane.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + controlPane.setMaxSize(300, 300); + controlPane.setHgap(20); + controlPane.setVgap(20); + controlPane.getRowConstraints().addAll( + new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false), + new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false), + new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false) + ); + + var topBtn = new Button("Top"); + topBtn.setOnAction(e -> { + modalPaneL1.setAlignment(Pos.TOP_CENTER); + modalPaneL1.usePredefinedTransitionFactories(Side.TOP); + modalPaneL1.show(getOrCreateTopDialog()); + }); + controlPane.add(topBtn, 1, 0); + + var rightBtn = new Button("Right"); + rightBtn.setOnAction(e -> { + modalPaneL1.setAlignment(Pos.TOP_RIGHT); + modalPaneL1.usePredefinedTransitionFactories(Side.RIGHT); + modalPaneL1.show(getOrCreateRightDialog()); + }); + controlPane.add(rightBtn, 2, 1); + + var centerBtn = new Button("Center"); + centerBtn.setOnAction(e -> { + modalPaneL1.setAlignment(Pos.CENTER); + modalPaneL1.usePredefinedTransitionFactories(null); + modalPaneL1.show(getOrCreateCenterDialog()); + }); + controlPane.add(centerBtn, 1, 1); + + var bottomBtn = new Button("Bottom"); + bottomBtn.setOnAction(e -> { + modalPaneL1.setAlignment(Pos.BOTTOM_CENTER); + modalPaneL1.usePredefinedTransitionFactories(Side.BOTTOM); + modalPaneL1.show(getOrCreateBottomDialog()); + }); + controlPane.add(bottomBtn, 1, 2); + + var leftBtn = new Button("Left"); + leftBtn.setOnAction(e -> { + modalPaneL1.setAlignment(Pos.TOP_LEFT); + modalPaneL1.usePredefinedTransitionFactories(Side.LEFT); + modalPaneL1.show(getOrCreateLeftDialog()); + }); + controlPane.add(leftBtn, 0, 1); + + controlPane.getChildren().forEach(c -> ((Button) c).setPrefWidth(100)); + + var root = new VBox(controlPane); + root.setAlignment(Pos.CENTER); + + return root; + } + + private Pane getOrCreateCenterDialog() { + if (centerDialog != null) { + return centerDialog; + } + + centerDialog = createGenericDialog(450, 450, e -> modalPaneL1.hide(true)); + + return centerDialog; + } + + private Node getOrCreateTopDialog() { + if (topDialog != null) { + return topDialog; + } + + topDialog = createGenericDialog(-1, 150, e -> modalPaneL1.hide(true)); + return topDialog; + } + + private Node getOrCreateRightDialog() { + if (rightDialog != null) { + return rightDialog; + } + + rightDialog = createGenericDialog(250, -1, e -> modalPaneL1.hide(true)); + return rightDialog; + } + + private Node getOrCreateBottomDialog() { + if (bottomDialog != null) { + return bottomDialog; + } + + bottomDialog = createGenericDialog(-1, 150, e -> modalPaneL1.hide(true)); + return bottomDialog; + } + + private Node getOrCreateLeftDialog() { + if (leftDialog != null) { + return leftDialog; + } + + leftDialog = createGenericDialog(250, -1, e -> modalPaneL1.hide(true)); + return leftDialog; + } + + private VBox createOverflowDialog() { + var dialog = createGenericDialog(400, 400, e1 -> modalPaneL1.hide(true)); + + var content = new VBox(); + for (int i = 0; i < 10; i++) { + var r = new Rectangle(600, 100); + if (i % 2 == 0) { + r.setFill(Color.AZURE); + } else { + r.setFill(Color.TOMATO); + } + content.getChildren().add(r); + } + + var scrollPane = new ScrollPane(); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setFitToHeight(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setFitToWidth(true); + scrollPane.setMaxHeight(20_000); + scrollPane.setContent(content); + + dialog.getChildren().setAll(scrollPane); + + return dialog; + } + + private VBox createFullScreenDialog() { + return createGenericDialog(-1, -1, e1 -> modalPaneL1.hide(true)); + } + + private VBox createLevel1Dialog() { + var dialog = createGenericDialog(600, 600, e1 -> modalPaneL1.hide(true)); + + var nextDialogBtn = new Button("Dialog 2"); + nextDialogBtn.setOnAction(e -> { + modalPaneL2.setAlignment(Pos.CENTER); + modalPaneL2.usePredefinedTransitionFactories(null); + modalPaneL2.show(createLevel2Dialog()); + }); + dialog.getChildren().add(nextDialogBtn); + + return dialog; + } + + private VBox createLevel2Dialog() { + var dialog = createGenericDialog(450, 450, e2 -> modalPaneL2.hide(true)); + + var nextDialogBtn = new Button("Dialog 3"); + nextDialogBtn.setOnAction(e -> { + modalPaneL3.setAlignment(Pos.CENTER); + modalPaneL3.usePredefinedTransitionFactories(null); + modalPaneL3.show(createLevel3Dialog()); + }); + dialog.getChildren().add(nextDialogBtn); + + return dialog; + } + + private VBox createLevel3Dialog() { + return createGenericDialog(300, 300, e2 -> modalPaneL3.hide(true)); + } + + private VBox createGenericDialog(double width, double height, EventHandler closeHandler) { + var dialog = new VBox(10); + dialog.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + dialog.setAlignment(Pos.CENTER); + dialog.setMinSize(width, height); + dialog.setMaxSize(width, height); + + var closeBtn = new Button("Close"); + closeBtn.setOnAction(closeHandler); + dialog.getChildren().add(closeBtn); + + return dialog; + } +} diff --git a/styles/src/general/_extras.scss b/styles/src/general/_extras.scss index 2fa438f..0b00167 100644 --- a/styles/src/general/_extras.scss +++ b/styles/src/general/_extras.scss @@ -14,6 +14,10 @@ $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 // ///////////////////////////////////////////////////////////////////////////////