Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand All @@ -144,18 +145,26 @@ 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) {
// Reopening skips the normal show layout; alignment needs the updated content size.
getScene().getRoot().applyCss();
getScene().getRoot().layout();
setAnchorX(anchorX);
setAnchorY(anchorY);
Comment thread
Wulian233 marked this conversation as resolved.
} 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());
}
}

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.");
}
Expand All @@ -168,10 +177,26 @@ 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)) {
super.hide();
return;
}

if (isShowing()) {
skin.animateClose(this::hideImmediately);
Comment thread
Wulian233 marked this conversation as resolved.
} else {
skin.init();
}
}

private void hideImmediately() {
super.hide();
((JFXPopupSkin) getSkin()).init();
}

/***************************************************************************
Expand Down
78 changes: 77 additions & 1 deletion HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
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.layout.*;
import javafx.scene.input.KeyEvent;
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;
Expand All @@ -49,10 +53,25 @@ public class JFXPopupSkin implements Skin<JFXPopup> {
protected Node root;

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);
Expand All @@ -74,6 +93,7 @@ public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX,
}

public final void animate() {
container.setMouseTransparent(false);
if (animation != null) {
if (animation.getStatus() == Status.STOPPED) {
container.setOpacity(1);
Expand All @@ -87,6 +107,51 @@ public final void animate() {
}
}

public final void animateClose(Runnable onFinished) {
if (closeAnimation != null) {
return;
}

container.setMouseTransparent(true);
Comment thread
Wulian233 marked this conversation as resolved.
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();
}

public final boolean cancelCloseAnimation() {
if (closeAnimation == null) {
return false;
}

closeAnimation.stop();
closeAnimation = null;
container.setMouseTransparent(false);
return true;
}

@Override
public JFXPopup getSkinnable() {
return control;
Expand All @@ -97,12 +162,18 @@ 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;
}
if (closeAnimation != null) {
closeAnimation.stop();
closeAnimation = null;
}
container = null;
control = null;
popupContent = null;
Expand Down Expand Up @@ -132,6 +203,11 @@ protected Animation getAnimation() {
public void init() {
if (animation != null)
animation.stop();
if (closeAnimation != null) {
closeAnimation.stop();
closeAnimation = null;
}
container.setMouseTransparent(false);
container.setOpacity(0);
scale.setX(1.0);
scale.setY(0.01);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +57,10 @@ public final class GameListCell extends ListCell<GameListItem> {

private final StringProperty tag = new SimpleStringProperty();

private JFXPopup managementPopup;

private GameListItem managementPopupItem;

public GameListCell() {
BorderPane root = new BorderPane();
root.getStyleClass().add("md-list-cell");
Expand Down Expand Up @@ -132,9 +140,22 @@ 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());
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"));
Expand All @@ -152,8 +173,9 @@ public void fire() {
item.modifyGameSettings();
}
} else if (e.getButton() == MouseButton.SECONDARY) {
JFXPopup popup = getPopup(item);
JFXPopup popup = getManagementPopup(item);
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());
}
});
Expand All @@ -168,6 +190,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();
Expand All @@ -193,7 +221,18 @@ 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);

Expand Down
Loading