diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java index 54c8d09ec0..4d65797117 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java @@ -19,25 +19,39 @@ import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.animation.KeyValue; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.transform.Scale; import javafx.stage.WindowEvent; +import javafx.util.Duration; import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.construct.ImageContainer; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; @@ -46,20 +60,68 @@ import java.util.List; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - +import static org.jackhuang.hmcl.ui.FXUtils.SINE; /// @author Glavo public final class GameListPopupMenu extends StackPane { private static final String KEY = GameListPopupMenu.class.getName() + ".popup"; + private static final String HIDING_KEY = GameListPopupMenu.class.getName() + ".hiding"; - public static boolean hideShowing(Node owner) { - JFXPopup popup = (JFXPopup) owner.getProperties().get(KEY); - if (popup != null && popup.isShowing()) { + public static void hideAnimated(JFXPopup popup) { + if (popup == null || !popup.isShowing()) { + return; + } + + if (Boolean.TRUE.equals(popup.getProperties().put(HIDING_KEY, true))) { + return; + } + + if (!AnimationUtils.isAnimationEnabled()) { + popup.hide(); + return; + } + + Node content = popup.getPopupContent(); + if (content == null) { popup.hide(); - return true; - } else { + return; + } + + Node container = content.getParent() != null ? content.getParent() : content; + Bounds bounds = container.getLayoutBounds(); + + Scale scaleTransform = new Scale(1.0, 1.0, bounds.getWidth(), bounds.getHeight()); + container.getTransforms().setAll(scaleTransform); + + Timeline closeAnimation = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(container.opacityProperty(), 1.0, SINE), + new KeyValue(scaleTransform.xProperty(), 1.0, SINE), + new KeyValue(scaleTransform.yProperty(), 1.0, SINE) + ), + new KeyFrame(Duration.millis(160), + new KeyValue(container.opacityProperty(), 0.0, SINE), + new KeyValue(scaleTransform.xProperty(), 0.0, SINE), + new KeyValue(scaleTransform.yProperty(), 0.0, SINE) + ) + ); + + closeAnimation.setOnFinished(event -> { + popup.hide(); + container.getTransforms().clear(); + container.setOpacity(1.0); + }); + + FXUtils.playAnimation(container, "popup-close", closeAnimation); + } + + public static boolean hideShowing(Node owner) { + if (!(owner.getProperties().get(KEY) instanceof JFXPopup popup && popup.isShowing())) { return false; } + + hideAnimated(popup); + return true; } /// Shows an instance selection popup relative to its owner. @@ -80,9 +142,68 @@ public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlig .toList()); JFXPopup popup = new JFXPopup(menu); owner.getProperties().put(KEY, popup); - popup.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> owner.getProperties().remove(KEY, popup)); - popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY, true); + popup.setAutoHide(false); + popup.setHideOnEscape(false); + Scene scene = owner.getScene(); + EventHandler outsideClickHandler = event -> { + if (popup.isShowing()) { + Bounds ownerBounds = owner.localToScreen(owner.getBoundsInLocal()); + if (ownerBounds != null && ownerBounds.contains(event.getScreenX(), event.getScreenY())) { + return; + } + + Bounds popupBounds = menu.localToScreen(menu.getBoundsInLocal()); + if (popupBounds != null && !popupBounds.contains(event.getScreenX(), event.getScreenY())) { + hideAnimated(popup); + } + } + }; + + EventHandler outsideScrollHandler = event -> { + if (popup.isShowing()) { + Bounds popupBounds = menu.localToScreen(menu.getBoundsInLocal()); + if (popupBounds != null && !popupBounds.contains(event.getScreenX(), event.getScreenY())) { + hideAnimated(popup); + } + } + }; + EventHandler escHandler = event -> { + if (event.getCode() == KeyCode.ESCAPE && popup.isShowing()) { + event.consume(); + hideAnimated(popup); + } + }; + + ChangeListener focusListener = (obs, wasFocused, isFocused) -> { + if (!isFocused && popup.isShowing()) { + hideAnimated(popup); + } + }; + + if (scene != null) { + scene.addEventFilter(MouseEvent.MOUSE_PRESSED, outsideClickHandler); + scene.addEventFilter(ScrollEvent.SCROLL, outsideScrollHandler); + scene.addEventFilter(KeyEvent.KEY_PRESSED, escHandler); + if (scene.getWindow() != null) { + scene.getWindow().focusedProperty().addListener(focusListener); + } + } + popup.focusedProperty().addListener(focusListener); + + popup.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> { + owner.getProperties().remove(KEY, popup); + if (scene != null) { + scene.removeEventFilter(MouseEvent.MOUSE_PRESSED, outsideClickHandler); + scene.removeEventFilter(ScrollEvent.SCROLL, outsideScrollHandler); + scene.removeEventFilter(KeyEvent.KEY_PRESSED, escHandler); + if (scene.getWindow() != null) { + scene.getWindow().focusedProperty().removeListener(focusListener); + } + } + popup.focusedProperty().removeListener(focusListener); + }); + popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY, false); return popup; } @@ -155,7 +276,7 @@ public Cell(ListView listView) { if (item != null) { item.getRepository().setSelectedInstance(new GameInstanceID(item.getId())); if (getScene().getWindow() instanceof JFXPopup popup) - popup.hide(); + hideAnimated(popup); } });