From 573e25ecdfd549701b7d44ae13e8c6eee6f31331 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 24 Dec 2025 20:38:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20JFXPopup=20=E5=9C=A8=20HiD?= =?UTF-8?q?PI=20=E6=98=BE=E7=A4=BA=E5=99=A8=E4=B8=8A=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E8=BF=87=E7=A8=8B=E4=B8=AD=E5=86=85=E5=AE=B9?= =?UTF-8?q?=20Y=20=E8=BD=B4=E6=96=B9=E5=90=91=E7=BC=A9=E6=94=BE=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E7=9A=84=E9=97=AE=E9=A2=98=20(#5064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/jfoenix/controls/JFXPopup.java | 171 ++++++++++++++++++ .../java/com/jfoenix/skins/JFXPopupSkin.java | 139 ++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java new file mode 100644 index 000000000..41accd48a --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPopup.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.skins.JFXPopupSkin; +import javafx.application.Platform; +import javafx.beans.DefaultProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.PopupControl; +import javafx.scene.control.Skin; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.stage.Window; + +/// JFXPopup is the material design implementation of a popup. +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-03-01 +@DefaultProperty(value = "popupContent") +public class JFXPopup extends PopupControl { + + public enum PopupHPosition { + RIGHT, LEFT + } + + public enum PopupVPosition { + TOP, BOTTOM + } + + /// Creates empty popup. + public JFXPopup() { + this(null); + } + + /// creates popup with a specified container and content + /// + /// @param content the node that will be shown in the popup + public JFXPopup(Region content) { + setPopupContent(content); + initialize(); + } + + private void initialize() { + this.setAutoFix(false); + this.setAutoHide(true); + this.setHideOnEscape(true); + this.setConsumeAutoHidingEvents(false); + this.getStyleClass().add(DEFAULT_STYLE_CLASS); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXPopupSkin(this); + } + + /*************************************************************************** + * * + * Setters / Getters * + * * + **************************************************************************/ + + private final ObjectProperty popupContent = new SimpleObjectProperty<>(new Pane()); + + public final ObjectProperty popupContentProperty() { + return this.popupContent; + } + + public final Region getPopupContent() { + return this.popupContentProperty().get(); + } + + public final void setPopupContent(final Region popupContent) { + this.popupContentProperty().set(popupContent); + } + + /*************************************************************************** + * * + * Public API * + * * + **************************************************************************/ + + /// show the popup using the default position + public void show(Node node) { + this.show(node, PopupVPosition.TOP, PopupHPosition.LEFT, 0, 0); + } + + /// show the popup according to the specified position + /// + /// @param vAlign can be TOP/BOTTOM + /// @param hAlign can be LEFT/RIGHT + public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign) { + this.show(node, vAlign, hAlign, 0, 0); + } + + /// show the popup according to the specified position with a certain offset + /// + /// @param vAlign can be TOP/BOTTOM + /// @param hAlign can be LEFT/RIGHT + /// @param initOffsetX on the x-axis + /// @param initOffsetY on the y-axis + public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) { + if (!isShowing()) { + if (node.getScene() == null || node.getScene().getWindow() == null) { + throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window."); + } + Window parent = node.getScene().getWindow(); + final Point2D origin = node.localToScene(0, 0); + final double anchorX = parent.getX() + origin.getX() + + node.getScene().getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0); + final double anchorY = parent.getY() + origin.getY() + + node.getScene() + .getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0); + this.show(parent, anchorX, anchorY); + ((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, 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()) { + if (window == null) { + throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window."); + } + Window parent = window; + final double anchorX = parent.getX() + x + initOffsetX; + final double anchorY = parent.getY() + y + initOffsetY; + this.show(parent, anchorX, anchorY); + ((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY); + Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate()); + } + } + + @Override + public void hide() { + super.hide(); + ((JFXPopupSkin) getSkin()).init(); + } + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /// Initialize the style class to 'jfx-popup'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-popup"; +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java new file mode 100644 index 000000000..cab45da10 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXPopupSkin.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016 JFoenix + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.jfoenix.skins; + +import com.jfoenix.controls.JFXPopup; +import com.jfoenix.controls.JFXPopup.PopupHPosition; +import com.jfoenix.controls.JFXPopup.PopupVPosition; +import com.jfoenix.effects.JFXDepthManager; +import javafx.animation.*; +import javafx.animation.Animation.Status; +import javafx.scene.Node; +import javafx.scene.control.Skin; +import javafx.scene.layout.*; +import javafx.scene.transform.Scale; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.Motion; + +/// # Material Design Popup Skin +/// TODO: REWORK +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-03-01 +public class JFXPopupSkin implements Skin { + + protected JFXPopup control; + protected StackPane container = new StackPane(); + protected Region popupContent; + protected Node root; + + private Animation animation; + protected Scale scale; + + public JFXPopupSkin(JFXPopup control) { + this.control = control; + // 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); + popupContent = control.getPopupContent(); + container.getStyleClass().add("jfx-popup-container"); + container.getChildren().add(popupContent); + container.getTransforms().add(scale); + container.setOpacity(0); + root = JFXDepthManager.createMaterialNode(container, 4); + animation = AnimationUtils.isAnimationEnabled() ? getAnimation() : null; + } + + public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, double offsetY) { + // postion the popup according to its animation + scale.setPivotX(hAlign == PopupHPosition.RIGHT ? container.getWidth() : 0); + scale.setPivotY(vAlign == PopupVPosition.BOTTOM ? container.getHeight() : 0); + root.setTranslateX(hAlign == PopupHPosition.RIGHT ? -container.getWidth() + offsetX : offsetX); + root.setTranslateY(vAlign == PopupVPosition.BOTTOM ? -container.getHeight() + offsetY : offsetY); + } + + public final void animate() { + if (animation != null) { + if (animation.getStatus() == Status.STOPPED) { + container.setOpacity(1); + animation.playFromStart(); + } + } else { + container.setOpacity(1); + popupContent.setOpacity(1); + scale.setX(1.0); + scale.setY(1.0); + } + } + + @Override + public JFXPopup getSkinnable() { + return control; + } + + @Override + public Node getNode() { + return root; + } + + @Override + public void dispose() { + if (animation != null) { + animation.stop(); + animation = null; + } + container = null; + control = null; + popupContent = null; + root = null; + } + + protected Animation getAnimation() { + Interpolator interpolator = Motion.EASE; + return new Timeline( + new KeyFrame( + Duration.ZERO, + new KeyValue(popupContent.opacityProperty(), 0, interpolator), + new KeyValue(scale.xProperty(), 0, interpolator), + new KeyValue(scale.yProperty(), 0, interpolator) + ), + new KeyFrame(Motion.SHORT4, + new KeyValue(popupContent.opacityProperty(), 0, interpolator), + new KeyValue(scale.xProperty(), 1, interpolator) + ), + new KeyFrame(Motion.MEDIUM2, + new KeyValue(popupContent.opacityProperty(), 1, interpolator), + new KeyValue(scale.yProperty(), 1, interpolator) + ) + ); + } + + public void init() { + if (animation != null) + animation.stop(); + container.setOpacity(0); + scale.setX(1.0); + scale.setY(0.01); + } +}