Add modal pane
This commit is contained in:
parent
0489d6c3e5
commit
a949420f6a
@ -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).
|
||||
|
265
base/src/main/java/atlantafx/base/controls/ModalPane.java
Executable file
265
base/src/main/java/atlantafx/base/controls/ModalPane.java
Executable file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
269
base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java
Normal file
269
base/src/main/java/atlantafx/base/controls/ModalPaneSkin.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
274
base/src/main/java/atlantafx/base/util/Animations.java
Executable file
274
base/src/main/java/atlantafx/base/util/Animations.java
Executable file
@ -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.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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
.modal-pane>.scroll-pane>.viewport>*>.scrollable-content {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// BBCode //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
Loading…
Reference in New Issue
Block a user