diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java b/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java index 05cad330efa..89547c3df04 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java @@ -25,10 +25,7 @@ import com.jfoenix.transitions.CachedTransition; import javafx.animation.*; import javafx.beans.DefaultProperty; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ObjectPropertyBase; -import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.*; import javafx.css.*; import javafx.event.Event; import javafx.event.EventHandler; @@ -45,11 +42,15 @@ import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.util.Duration; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.animation.Motion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /// Note: for JFXDialog to work properly, the root node **MUST** /// be of type [StackPane] @@ -119,7 +120,12 @@ public JFXDialog(StackPane dialogContainer, Region content, DialogTransition tra /// - BOTTOM /// - LEFT public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType, boolean overlayClose) { + this(dialogContainer, content, null, transitionType, overlayClose); + } + + public JFXDialog(StackPane dialogContainer, Region content, @Nullable StackPane overlayPane, DialogTransition transitionType, boolean overlayClose) { setOverlayClose(overlayClose); + setOverlayPane(overlayPane); initialize(); setContent(content); setDialogContainer(dialogContainer); @@ -129,35 +135,55 @@ public JFXDialog(StackPane dialogContainer, Region content, DialogTransition tra } private void initChangeListeners() { - overlayCloseProperty().addListener((o, oldVal, newVal) -> { - if (newVal) { - this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + FXUtils.onChange(overlayCloseProperty(), b -> { + if (b) { + getOverlayPane().addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); } else { - this.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + getOverlayPane().removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + } + }); + this.overlayPaneProperty().addListener((observable, oldValue, newValue) -> { + if (oldValue == null) oldValue = JFXDialog.this; + if (newValue == null) newValue = JFXDialog.this; + if (oldValue == newValue) return; + oldValue.getStyleClass().remove("jfx-dialog-overlay-pane"); + oldValue.setBackground(null); + oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + newValue.getStyleClass().add("jfx-dialog-overlay-pane"); + newValue.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null))); + // close the dialog if clicked on the overlay pane + if (overlayClose.get()) { + newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); } }); } private void initialize() { - this.setVisible(false); this.getStyleClass().add(DEFAULT_STYLE_CLASS); - this.transitionType.addListener((o, oldVal, newVal) -> { - animation = getShowAnimation(transitionType.get()); - }); + this.setVisible(false); + + FXUtils.onChange(overlayPane, t -> animation = getShowAnimation(transitionType.get())); + FXUtils.onChange(transitionType, t -> animation = getShowAnimation(transitionType.get())); contentHolder = new StackPane(); + contentHolder.setVisible(false); JFXDepthManager.setDepth(contentHolder, 4); contentHolder.setPickOnBounds(false); // ensure stackpane is never resized beyond it's preferred size contentHolder.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); this.getChildren().add(contentHolder); - this.getStyleClass().add("jfx-dialog-overlay-pane"); StackPane.setAlignment(contentHolder, Pos.CENTER); - this.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null))); + + FXUtils.onChangeAndOperate(overlayPane, t -> setPickOnBounds(t == this || t == null)); + + getOverlayPane().setVisible(false); + getOverlayPane().getStyleClass().add("jfx-dialog-overlay-pane"); + getOverlayPane().setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null))); // close the dialog if clicked on the overlay pane if (overlayClose.get()) { - this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + getOverlayPane().addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); } + // prevent propagating the events to overlay pane contentHolder.addEventHandler(MouseEvent.ANY, Event::consume); } @@ -214,6 +240,21 @@ public final void setOverlayClose(final boolean overlayClose) { this.overlayCloseProperty().set(overlayClose); } + private final ObjectProperty overlayPane = new SimpleObjectProperty<>(this); + + public final ObjectProperty overlayPaneProperty() { + return this.overlayPane; + } + + @NotNull + public final StackPane getOverlayPane() { + return Objects.requireNonNullElse(this.overlayPaneProperty().get(), this); + } + + public final void setOverlayPane(final StackPane overlayPane) { + this.overlayPaneProperty().set(overlayPane == null ? this : overlayPane); + } + /// if sets to true, the content of dialog container will be cached and replaced with an image /// when displaying the dialog (better performance). /// this is recommended if the content behind the dialog will not change during the showing @@ -274,8 +315,11 @@ private void showDialog() { if (animation != null) { animation.play(); } else { - setVisible(true); - setOpacity(1); + this.setVisible(true); + contentHolder.setVisible(true); + contentHolder.setOpacity(1); + getOverlayPane().setVisible(true); + getOverlayPane().setOpacity(1); Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED)); } } @@ -291,8 +335,11 @@ public void close() { closeDialog(); }); } else { - setOpacity(0); - setVisible(false); + this.setVisible(false); + contentHolder.setOpacity(0); + contentHolder.setVisible(false); + getOverlayPane().setOpacity(0); + getOverlayPane().setVisible(false); closeDialog(); } } @@ -340,6 +387,8 @@ private Transition getShowAnimation(DialogTransition transitionType) { private void resetProperties() { this.setVisible(false); + contentHolder.setVisible(false); + getOverlayPane().setVisible(false); contentHolder.setTranslateX(0); contentHolder.setTranslateY(0); contentHolder.setScaleX(1); @@ -354,17 +403,25 @@ private final class CenterTransition extends CachedTransition { new KeyFrame(Duration.ZERO, new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR), new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR), - new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR) + new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR), + new KeyValue(contentHolder.visibleProperty(), false, Motion.LINEAR), + new KeyValue(getOverlayPane().visibleProperty(), false, Motion.LINEAR) ), new KeyFrame(Duration.millis(10), new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR), - new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR) + new KeyValue(contentHolder.visibleProperty(), true, Motion.LINEAR), + new KeyValue(contentHolder.opacityProperty(), 0, INTERPOLATOR), + new KeyValue(getOverlayPane().visibleProperty(), true, Motion.LINEAR), + new KeyValue(getOverlayPane().opacityProperty(), 0, INTERPOLATOR) ), new KeyFrame(Motion.EXTRA_LONG4, new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR), new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR), new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR), - new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR) + new KeyValue(contentHolder.visibleProperty(), true, Motion.LINEAR), + new KeyValue(contentHolder.opacityProperty(), 1, INTERPOLATOR), + new KeyValue(getOverlayPane().visibleProperty(), true, Motion.LINEAR), + new KeyValue(getOverlayPane().opacityProperty(), 1, INTERPOLATOR) )) ); // reduce the number to increase the shifting , increase number to reduce shifting diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java index c54af932626..c84d6fe3fda 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java @@ -55,12 +55,13 @@ private DialogUtils() { /// @param content the dialog content public static void show(Decorator decorator, Node content) { StackPane dialogContainer = decorator.getDialogContainer(); + StackPane dialogOverlayPane = decorator.getDialogOverlayPane(); if (decorator.getRoot().getScene() == null) { - Platform.runLater(() -> showInDecorator(decorator, dialogContainer, content)); + Platform.runLater(() -> showInDecorator(decorator, dialogContainer, content, dialogOverlayPane)); return; } - showInDecorator(decorator, dialogContainer, content); + showInDecorator(decorator, dialogContainer, content, dialogOverlayPane); } /// Shows content in a decorator's resolved dialog container. @@ -68,8 +69,9 @@ public static void show(Decorator decorator, Node content) { /// @param decorator the main-window decorator /// @param dialogContainer the container resolved before any deferred execution /// @param content the dialog content - private static void showInDecorator(Decorator decorator, StackPane dialogContainer, Node content) { - show(dialogContainer, content, dialog -> { + /// @param dialogOverlayPane the dialog overlay pane + private static void showInDecorator(Decorator decorator, StackPane dialogContainer, Node content, StackPane dialogOverlayPane) { + show(dialogContainer, content, dialogOverlayPane, dialog -> { JFXDialogPane pane = (JFXDialogPane) dialog.getContent(); decorator.capableDraggingWindow(dialog); decorator.forbidDraggingWindow(pane); @@ -78,24 +80,24 @@ private static void showInDecorator(Decorator decorator, StackPane dialogContain } public static void show(StackPane container, Node content) { - show(container, content, null); + show(container, content, null, null); } - public static void show(StackPane container, Node content, @Nullable Consumer onDialogCreated) { + public static void show(StackPane container, Node content, @Nullable StackPane overlayPane, @Nullable Consumer onDialogCreated) { FXUtils.checkFxUserThread(); JFXDialog dialog = (JFXDialog) container.getProperties().get(PROPERTY_DIALOG_INSTANCE); JFXDialogPane dialogPane = (JFXDialogPane) container.getProperties().get(PROPERTY_DIALOG_PANE_INSTANCE); if (dialog == null) { - dialog = new JFXDialog(AnimationUtils.isAnimationEnabled() - ? JFXDialog.DialogTransition.CENTER - : JFXDialog.DialogTransition.NONE); dialogPane = new JFXDialogPane(); - - dialog.setContent(dialogPane); - dialog.setDialogContainer(container); - dialog.setOverlayClose(false); + dialog = new JFXDialog( + container, + dialogPane, + overlayPane, + AnimationUtils.isAnimationEnabled() ? JFXDialog.DialogTransition.CENTER : JFXDialog.DialogTransition.NONE, + false + ); container.getProperties().put(PROPERTY_DIALOG_INSTANCE, dialog); container.getProperties().put(PROPERTY_DIALOG_PANE_INSTANCE, dialogPane); @@ -145,7 +147,8 @@ public void changed(ObservableValue observable, Boolean oldVa /// @param content the dialog content public static void showLater(Decorator decorator, Node content) { StackPane dialogContainer = decorator.getDialogContainer(); - Runnable showDialogAction = () -> showInDecorator(decorator, dialogContainer, content); + StackPane dialogOverlayPane = decorator.getDialogOverlayPane(); + Runnable showDialogAction = () -> showInDecorator(decorator, dialogContainer, content, dialogOverlayPane); if (decorator.getRoot().getScene() == null) { Platform.runLater(() -> showLater(dialogContainer, showDialogAction)); return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index 8638d30640b..a66e5653620 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -330,6 +330,13 @@ public StackPane getDialogContainer() { return mainWindowPane; } + /// Returns the pane that behaves as the overlay of dialogs. + /// + /// @return the dialog overlay pane + public StackPane getDialogOverlayPane() { + return mainWindowPane.getDialogOverlayPane(); + } + /// Returns the navigation stack rendered by the main window. /// /// @return this decorator's navigator @@ -1111,5 +1118,4 @@ private void resetRootTransform() { root.setScaleY(1); root.setScaleZ(1); } - } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/MainWindowPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/MainWindowPane.java index 106d7ec2c5f..c96e2d2f223 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/MainWindowPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/MainWindowPane.java @@ -28,15 +28,7 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; +import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.util.Duration; @@ -75,6 +67,9 @@ final class MainWindowPane extends StackPane { /// The transition container used when the title-bar state changes. private final TransitionPane navBarPane; + /// The pane that behaves as the overlay of dialogs. + private final StackPane dialogOverlayPane; + /// Retains listener delegates that are registered through weak listener wrappers. @SuppressWarnings("FieldCanBeLocal") private final WeakListenerHolder holder = new WeakListenerHolder(); @@ -112,11 +107,12 @@ final class MainWindowPane extends StackPane { center.getChildren().setAll(decorator.getNavigator()); frame.setCenter(center); - HBox rightButtonsContainer = createWindowButtons(); + Rectangle buttonsPlaceholder = new Rectangle(); + buttonsPlaceholder.setFill(null); titleBar = new BorderPane(); titleBar.setPickOnBounds(false); titleBar.getStyleClass().add("jfx-tool-bar"); - titleBar.setRight(rightButtonsContainer); + titleBar.setRight(buttonsPlaceholder); navBarPane = new TransitionPane(); titleBar.setCenter(navBarPane); @@ -133,7 +129,18 @@ final class MainWindowPane extends StackPane { decorator.capableDraggingWindow(titleBar); - getChildren().setAll(backgroundNode, frame); + dialogOverlayPane = new StackPane(); + dialogOverlayPane.setVisible(false); + + HBox rightButtonsContainer = createWindowButtons(); + AnchorPane buttonsLayer = new AnchorPane(rightButtonsContainer); + buttonsLayer.setPickOnBounds(false); + AnchorPane.setTopAnchor(rightButtonsContainer, 0D); + AnchorPane.setRightAnchor(rightButtonsContainer, 0D); + buttonsPlaceholder.heightProperty().bind(rightButtonsContainer.heightProperty()); + buttonsPlaceholder.widthProperty().bind(rightButtonsContainer.widthProperty()); + + getChildren().setAll(backgroundNode, frame, dialogOverlayPane, buttonsLayer); } /// Updates the content-corner shape for an edge-to-edge window state. @@ -312,6 +319,10 @@ private Node createNavBar(DecoratorPage.State state) { return navBar; } + public StackPane getDialogOverlayPane() { + return dialogOverlayPane; + } + /// Produces directional transitions for page-title changes. private enum NavBarAnimations implements TransitionPane.AnimationProducer { /// Moves the next title in from the right.