From e3049b756b8dbd3014d7060e43237e3fe1acad80 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 9 Aug 2026 23:24:16 +0800 Subject: [PATCH 1/2] [Enhancement] add animated hiding functionality for game list popup menu --- .../hmcl/ui/instances/GameListPopupMenu.java | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) 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..39626f1f21 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,11 +19,15 @@ 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.collections.ObservableList; +import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -33,11 +37,14 @@ 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 +53,69 @@ 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 (!AnimationUtils.isAnimationEnabled()) { popup.hide(); - return true; - } else { + return; + } + + Node content = popup.getPopupContent(); + if (content == null) { + popup.hide(); + 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; } + + if (Boolean.TRUE.equals(popup.getProperties().get(HIDING_KEY))) { + return true; + } + + popup.getProperties().put(HIDING_KEY, true); + hideAnimated(popup); + return true; } /// Shows an instance selection popup relative to its owner. From 8b79a64245d2cdae83d8f516f7d93395904f8c4c Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 9 Aug 2026 23:24:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ui)=20=E6=B7=BB=E5=8A=A0=E4=BA=86Popup?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E7=82=B9=E5=87=BB=E6=97=B6=E7=9A=84=E6=94=B6?= =?UTF-8?q?=E8=B5=B7=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/GameListPopupMenu.java | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) 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..97b3452b1e 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,35 @@ 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.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.MouseEvent; 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 +56,69 @@ 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 (!AnimationUtils.isAnimationEnabled()) { popup.hide(); - return true; - } else { + return; + } + + Node content = popup.getPopupContent(); + if (content == null) { + popup.hide(); + 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; } + + if (Boolean.TRUE.equals(popup.getProperties().get(HIDING_KEY))) { + return true; + } + + popup.getProperties().put(HIDING_KEY, true); + hideAnimated(popup); + return true; } /// Shows an instance selection popup relative to its owner. @@ -80,9 +139,42 @@ 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); + Scene scene = owner.getScene(); + EventHandler outsideClickHandler = event -> { + if (popup.isShowing()) { + Bounds popupBounds = menu.localToScreen(menu.getBoundsInLocal()); + if (popupBounds != null && !popupBounds.contains(event.getScreenX(), event.getScreenY())) { + hideAnimated(popup); + } + } + }; + javafx.beans.value.ChangeListener focusListener = (obs, wasFocused, isFocused) -> { + if (!isFocused && popup.isShowing()) { + hideAnimated(popup); + } + }; + + if (scene != null) { + scene.addEventFilter(MouseEvent.MOUSE_PRESSED, outsideClickHandler); + 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); + 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 +247,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); } });