Add modal pane

This commit is contained in:
mkpaz 2023-05-01 21:32:55 +04:00
parent 0489d6c3e5
commit a949420f6a
7 changed files with 1055 additions and 0 deletions

@ -8,6 +8,7 @@
- (Base) New `MaskTextField` (and `MaskTextFormatter`) component to support masked text input. - (Base) New `MaskTextField` (and `MaskTextFormatter`) component to support masked text input.
- (Base) New `PasswordTextField` component to simplify `PasswordTextFormatter` usage. - (Base) New `PasswordTextField` component to simplify `PasswordTextFormatter` usage.
- (Base) 🚀 [BBCode](https://ru.wikipedia.org/wiki/BBCode) markup support. - (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 MacOS-like Cupertino theme in light and dark variants.
- (CSS) 🚀 New [Dracula](https://ui.draculatheme.com/) theme. - (CSS) 🚀 New [Dracula](https://ui.draculatheme.com/) theme.
- (CSS) New `TabPane` style. There are three styles supported: default, floating and classic (new one). - (CSS) New `TabPane` style. There are three styles supported: default, floating and classic (new one).

@ -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.<br/><br/>
*
* <p>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<Node> 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<Node> 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<Pos> 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<Pos> alignmentProperty() {
return alignment;
}
// ~
protected ObjectProperty<Function<Node, Animation>> inTransitionFactory = new SimpleObjectProperty<>(
this, "inTransitionFactory", node -> Animations.zoomIn(node, DEFAULT_DURATION_IN)
);
public Function<Node, Animation> getInTransitionFactory() {
return inTransitionFactory.get();
}
public void setInTransitionFactory(Function<Node, Animation> 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<Function<Node, Animation>> inTransitionFactoryProperty() {
return inTransitionFactory;
}
// ~
protected ObjectProperty<Function<Node, Animation>> outTransitionFactory = new SimpleObjectProperty<>(
this, "outTransitionFactory", node -> Animations.zoomOut(node, DEFAULT_DURATION_OUT)
);
public Function<Node, Animation> getOutTransitionFactory() {
return outTransitionFactory.get();
}
public void setOutTransitionFactory(Function<Node, Animation> 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<Function<Node, Animation>> 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));
}
}
}
}
}

@ -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<ModalPane> {
protected ModalPane control;
protected final StackPane root;
protected final ScrollPane scrollPane;
protected final StackPane contentWrapper;
protected final EventHandler<KeyEvent> keyHandler = createKeyHandler();
protected final EventHandler<MouseEvent> mouseHandler = createMouseHandler();
protected final ChangeListener<Animation.Status> animationInListener = createAnimationInListener();
protected final ChangeListener<Animation.Status> animationOutListener = createAnimationOutListener();
protected @Nullable List<ScrollBar> 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<KeyEvent> createKeyHandler() {
return event -> {
if (event.getCode() == KeyCode.ESCAPE) {
if (getSkinnable().getPersistent()) {
createCloseBlockedAnimation().playFromStart();
} else {
hideAndConsume(event);
}
}
};
}
protected EventHandler<MouseEvent> 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<Animation.Status> createAnimationInListener() {
return (obs, old, val) -> {
if (val == Animation.Status.RUNNING) {
doShow();
}
};
}
protected ChangeListener<Animation.Status> 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);
}
}

@ -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;
}
}

@ -23,6 +23,7 @@ import atlantafx.sampler.page.components.LabelPage;
import atlantafx.sampler.page.components.ListPage; import atlantafx.sampler.page.components.ListPage;
import atlantafx.sampler.page.components.MenuButtonPage; import atlantafx.sampler.page.components.MenuButtonPage;
import atlantafx.sampler.page.components.MenuPage; import atlantafx.sampler.page.components.MenuPage;
import atlantafx.sampler.page.components.ModalPanePage;
import atlantafx.sampler.page.components.OverviewPage; import atlantafx.sampler.page.components.OverviewPage;
import atlantafx.sampler.page.components.PaginationPage; import atlantafx.sampler.page.components.PaginationPage;
import atlantafx.sampler.page.components.PopoverPage; import atlantafx.sampler.page.components.PopoverPage;
@ -190,6 +191,7 @@ public class MainModel {
NAV_TREE.get(BBCodePage.class), NAV_TREE.get(BBCodePage.class),
NAV_TREE.get(BreadcrumbsPage.class), NAV_TREE.get(BreadcrumbsPage.class),
NAV_TREE.get(CustomTextFieldPage.class), NAV_TREE.get(CustomTextFieldPage.class),
NAV_TREE.get(ModalPanePage.class),
NAV_TREE.get(PopoverPage.class), NAV_TREE.get(PopoverPage.class),
NAV_TREE.get(ToggleSwitchPage.class) NAV_TREE.get(ToggleSwitchPage.class)
); );
@ -245,6 +247,7 @@ public class MainModel {
MenuButtonPage.NAME, MenuButtonPage.NAME,
MenuButtonPage.class, "SplitMenuButton") 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(PaginationPage.class, NavTree.Item.page(PaginationPage.NAME, PaginationPage.class));
map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class)); map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class));
map.put(ProgressPage.class, NavTree.Item.page(ProgressPage.NAME, ProgressPage.class)); map.put(ProgressPage.class, NavTree.Item.page(ProgressPage.NAME, ProgressPage.class));

@ -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<ActionEvent> 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;
}
}

@ -14,6 +14,10 @@ $radius in cfg.$elevation {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive); @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 // // BBCode //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////