From 0a464b859cf91ab40f9201038e9eda194e12a0e4 Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Sun, 13 Sep 2026 10:20:01 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8DJFXPopup=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E5=85=B3=E9=97=AD=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/jfoenix/controls/JFXPopup.java | 14 +++++- .../java/com/jfoenix/skins/JFXPopupSkin.java | 50 +++++++++++++++++++ .../hmcl/ui/instances/GameListCell.java | 38 ++++++++++++-- 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java index 659c252ff94..bf405a236a0 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java @@ -170,8 +170,20 @@ public void show(Window window, double x, double y, PopupVPosition vAlign, Popup @Override public void hide() { + if (!(getSkin() instanceof JFXPopupSkin skin)) { + super.hide(); + return; + } + + if (isShowing()) { + skin.animateClose(this::hideImmediately); + } else { + skin.init(); + } + } + + private void hideImmediately() { super.hide(); - ((JFXPopupSkin) getSkin()).init(); } /*************************************************************************** diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java index c1daa775acb..a8ee9767cf5 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java @@ -49,6 +49,9 @@ public class JFXPopupSkin implements Skin { protected Node root; private Animation animation; + private Animation closeAnimation; + private boolean closing; + protected Scale scale; public JFXPopupSkin(JFXPopup control) { @@ -74,6 +77,8 @@ public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, } public final void animate() { + closing = false; + container.setMouseTransparent(false); if (animation != null) { if (animation.getStatus() == Status.STOPPED) { container.setOpacity(1); @@ -87,6 +92,41 @@ public final void animate() { } } + public final void animateClose(Runnable onFinished) { + if (closing) { + return; + } + + closing = true; + container.setMouseTransparent(true); + if (animation != null) { + animation.stop(); + } + + if (!AnimationUtils.isAnimationEnabled()) { + onFinished.run(); + init(); + return; + } + + Interpolator interpolator = Motion.EASE; + closeAnimation = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(popupContent.opacityProperty(), popupContent.getOpacity(), interpolator), + new KeyValue(scale.xProperty(), scale.getX(), interpolator), + new KeyValue(scale.yProperty(), scale.getY(), interpolator)), + new KeyFrame(Motion.SHORT4, + new KeyValue(popupContent.opacityProperty(), 0, interpolator), + new KeyValue(scale.xProperty(), 0, interpolator), + new KeyValue(scale.yProperty(), 0.01, interpolator))); + closeAnimation.setOnFinished(event -> { + closeAnimation = null; + onFinished.run(); + init(); + }); + closeAnimation.play(); + } + @Override public JFXPopup getSkinnable() { return control; @@ -103,6 +143,10 @@ public void dispose() { animation.stop(); animation = null; } + if (closeAnimation != null) { + closeAnimation.stop(); + closeAnimation = null; + } container = null; control = null; popupContent = null; @@ -132,6 +176,12 @@ protected Animation getAnimation() { public void init() { if (animation != null) animation.stop(); + if (closeAnimation != null) { + closeAnimation.stop(); + closeAnimation = null; + } + closing = false; + container.setMouseTransparent(false); container.setOpacity(0); scale.setX(1.0); scale.setY(0.01); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java index e3121aa88c4..26e245f5c44 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java @@ -53,6 +53,10 @@ public final class GameListCell extends ListCell { private final StringProperty tag = new SimpleStringProperty(); + private JFXPopup managementPopup; + + private GameListItem managementPopupItem; + public GameListCell() { BorderPane root = new BorderPane(); root.getStyleClass().add("md-list-cell"); @@ -132,7 +136,12 @@ public void fire() { if (item == null) return; - JFXPopup popup = getPopup(item); + JFXPopup popup = getManagementPopup(item); + if (popup.isShowing()) { + popup.hide(); + return; + } + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); }); @@ -152,7 +161,12 @@ public void fire() { item.modifyGameSettings(); } } else if (e.getButton() == MouseButton.SECONDARY) { - JFXPopup popup = getPopup(item); + JFXPopup popup = getManagementPopup(item); + if (popup.isShowing()) { + popup.hide(); + return; + } + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); popup.show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); } @@ -168,6 +182,12 @@ public void updateItem(GameListItem item, boolean empty) { if (oldItem == item && oldEmpty == empty) return; + if (managementPopup != null) { + managementPopup.hide(); + managementPopup = null; + managementPopupItem = null; + } + this.graphic.releaseRippleImmediately(); this.imageView.imageProperty().unbind(); @@ -193,9 +213,21 @@ public void updateItem(GameListItem item, boolean empty) { } } - private static JFXPopup getPopup(GameListItem item) { + private JFXPopup getManagementPopup(GameListItem item) { + if (managementPopup == null || managementPopupItem != item) { + if (managementPopup != null) { + managementPopup.hide(); + } + managementPopup = createPopup(item); + managementPopupItem = item; + } + return managementPopup; + } + + private static JFXPopup createPopup(GameListItem item) { PopupMenu menu = new PopupMenu(); JFXPopup popup = new JFXPopup(menu); + popup.setConsumeAutoHidingEvents(true); menu.getContent().setAll( new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), item::testGame, popup), From 0d41d80b50f40f6d6cf831dbdb2b32c0bb0d32c7 Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Mon, 21 Sep 2026 11:22:54 +0800 Subject: [PATCH 2/6] fix --- .../java/com/jfoenix/controls/JFXPopup.java | 18 +++++++++++++---- .../java/com/jfoenix/skins/JFXPopupSkin.java | 20 +++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java index bf405a236a0..b1108f27fb8 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java @@ -131,7 +131,8 @@ public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double } public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) { - if (!isShowing()) { + boolean relocate = cancelCloseAnimation(); + if (!isShowing() || relocate) { Scene scene = node.getScene(); if (scene == null || scene.getWindow() == null) { throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window."); @@ -144,10 +145,14 @@ public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0); double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0); - if (attachToNode) + if (relocate) { + setAnchorX(anchorX); + setAnchorY(anchorY); + } else if (attachToNode) { this.show(node, anchorX, anchorY); - else + } else { this.show(parent, anchorX, anchorY); + } ((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY); Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate()); @@ -155,7 +160,8 @@ public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double } public void show(Window window, double x, double y, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) { - if (!isShowing()) { + boolean relocate = cancelCloseAnimation(); + if (!isShowing() || relocate) { if (window == null) { throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window."); } @@ -168,6 +174,10 @@ public void show(Window window, double x, double y, PopupVPosition vAlign, Popup } } + public boolean cancelCloseAnimation() { + return isShowing() && getSkin() instanceof JFXPopupSkin skin && skin.cancelCloseAnimation(); + } + @Override public void hide() { if (!(getSkin() instanceof JFXPopupSkin skin)) { diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java index a8ee9767cf5..d503dcaf3e5 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java @@ -29,7 +29,8 @@ import javafx.animation.Animation.Status; import javafx.scene.Node; import javafx.scene.control.Skin; -import javafx.scene.layout.*; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.scene.transform.Scale; import javafx.util.Duration; import org.jackhuang.hmcl.ui.animation.AnimationUtils; @@ -50,7 +51,6 @@ public class JFXPopupSkin implements Skin { private Animation animation; private Animation closeAnimation; - private boolean closing; protected Scale scale; @@ -77,7 +77,6 @@ public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, } public final void animate() { - closing = false; container.setMouseTransparent(false); if (animation != null) { if (animation.getStatus() == Status.STOPPED) { @@ -93,11 +92,10 @@ public final void animate() { } public final void animateClose(Runnable onFinished) { - if (closing) { + if (closeAnimation != null) { return; } - closing = true; container.setMouseTransparent(true); if (animation != null) { animation.stop(); @@ -127,6 +125,17 @@ public final void animateClose(Runnable onFinished) { closeAnimation.play(); } + public final boolean cancelCloseAnimation() { + if (closeAnimation == null) { + return false; + } + + closeAnimation.stop(); + closeAnimation = null; + container.setMouseTransparent(false); + return true; + } + @Override public JFXPopup getSkinnable() { return control; @@ -180,7 +189,6 @@ public void init() { closeAnimation.stop(); closeAnimation = null; } - closing = false; container.setMouseTransparent(false); container.setOpacity(0); scale.setX(1.0); From 6617131087a6745b97c0ef9cd781dc84fd7fac3f Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Sat, 10 Oct 2026 20:01:07 +0800 Subject: [PATCH 3/6] =?UTF-8?q?Fix=E5=85=B3=E9=97=AD=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E6=9C=9F=E9=97=B4=E4=BB=8D=E4=BC=9A=E5=A4=84=E7=90=86=E9=94=AE?= =?UTF-8?q?=E7=9B=98=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/jfoenix/skins/JFXPopupSkin.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java index d503dcaf3e5..da9bb9a5f4c 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java @@ -25,10 +25,13 @@ import com.jfoenix.controls.JFXPopup.PopupHPosition; import com.jfoenix.controls.JFXPopup.PopupVPosition; import com.jfoenix.effects.JFXDepthManager; +import com.sun.javafx.event.RedirectedEvent; import javafx.animation.*; import javafx.animation.Animation.Status; +import javafx.event.EventDispatcher; import javafx.scene.Node; import javafx.scene.control.Skin; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.transform.Scale; @@ -51,11 +54,24 @@ public class JFXPopupSkin implements Skin { private Animation animation; private Animation closeAnimation; + private final EventDispatcher eventDispatcher; protected Scale scale; public JFXPopupSkin(JFXPopup control) { this.control = control; + eventDispatcher = control.getEventDispatcher(); + if (eventDispatcher != null) { + control.setEventDispatcher((event, tail) -> { + if (container.isMouseTransparent() + && (event instanceof KeyEvent + || event instanceof RedirectedEvent redirected && redirected.getOriginalEvent() instanceof KeyEvent)) { + // Leave redirected keys unconsumed so the owner window can handle them. + return event; + } + return eventDispatcher.dispatchEvent(event, tail); + }); + } // set scale y to 0.01 instead of 0 to allow layout of the content, // otherwise it will cause exception in traverse engine, when focusing the 1st node scale = new Scale(1.0, 0.01, 0, 0); @@ -146,8 +162,10 @@ public Node getNode() { return root; } + /// Stops animations, restores the popup dispatcher and releases the skin nodes. @Override public void dispose() { + control.setEventDispatcher(eventDispatcher); if (animation != null) { animation.stop(); animation = null; From 1dcc8d79fbb2cf3a810902d8c0282b3467b37610 Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Tue, 13 Oct 2026 20:23:50 +0800 Subject: [PATCH 4/6] Fix --- HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java index b1108f27fb8..89e3c56b954 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java @@ -146,6 +146,9 @@ public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0); if (relocate) { + // Reopening skips the normal show layout; alignment needs the updated content size. + getScene().getRoot().applyCss(); + getScene().getRoot().layout(); setAnchorX(anchorX); setAnchorY(anchorY); } else if (attachToNode) { From 88230e2abc0cfbc2847cd75eff0c6a4dc5692773 Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Wed, 21 Oct 2026 12:45:43 +0800 Subject: [PATCH 5/6] Fix --- .../hmcl/ui/instances/GameListCell.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java index 26e245f5c44..690cec2c914 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java @@ -17,11 +17,15 @@ */ package org.jackhuang.hmcl.ui.instances; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXPopup; +import com.jfoenix.controls.JFXRadioButton; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.event.ActionEvent; import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.ListCell; @@ -143,7 +147,15 @@ public void fire() { } JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); - popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); + boolean below = vPosition == JFXPopup.PopupVPosition.TOP; + Point2D offset = root.localToScene(0, 0).subtract(btnManage.localToScene(0, 0)); + double offsetX = offset.getX() + root.getWidth() - btnManage.getWidth(); + double offsetY = offset.getY() + (below ? root.getHeight() : -btnManage.getHeight()); + if (btnManage.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) + offsetX = -offsetX; + // Only this button bypasses auto-hide, so its action can toggle the menu. + // Keep the popup aligned to the cell while making the button its owner. + popup.show(btnManage, vPosition, JFXPopup.PopupHPosition.RIGHT, offsetX, offsetY, true); }); BorderPane.setAlignment(btnManage, Pos.CENTER); FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")); @@ -227,7 +239,6 @@ private JFXPopup getManagementPopup(GameListItem item) { private static JFXPopup createPopup(GameListItem item) { PopupMenu menu = new PopupMenu(); JFXPopup popup = new JFXPopup(menu); - popup.setConsumeAutoHidingEvents(true); menu.getContent().setAll( new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), item::testGame, popup), From 2c581e543082842776b18e88ad55c1ba6b7cce8f Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Fri, 23 Oct 2026 13:03:19 +0800 Subject: [PATCH 6/6] Fix --- .../java/org/jackhuang/hmcl/ui/instances/GameListCell.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java index 690cec2c914..cbf623ab821 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java @@ -174,12 +174,8 @@ public void fire() { } } else if (e.getButton() == MouseButton.SECONDARY) { JFXPopup popup = getManagementPopup(item); - if (popup.isShowing()) { - popup.hide(); - return; - } - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); + // Repeated right-clicks reopen and reposition even during the closing animation popup.show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); } });