创建 SVGContainer 控件 (#5464)
This commit is contained in:
@@ -18,8 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
@@ -30,6 +28,7 @@ import javafx.scene.shape.SVGPath;
|
||||
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
/// The view boxes of all icons are normalized to `0 0 24 24`.
|
||||
public enum SVG {
|
||||
NONE(""), // Empty Icon
|
||||
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
|
||||
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
|
||||
@@ -130,6 +129,12 @@ public enum SVG {
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
|
||||
static void setSize(SVGPath path, double size) {
|
||||
double scale = size / DEFAULT_SIZE;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
}
|
||||
|
||||
private final String rawPath;
|
||||
private String path;
|
||||
|
||||
@@ -144,35 +149,20 @@ public enum SVG {
|
||||
return path;
|
||||
}
|
||||
|
||||
public SVGPath createSVGPath() {
|
||||
var p = new SVGPath();
|
||||
p.setContent(getPath());
|
||||
p.getStyleClass().add("svg");
|
||||
return p;
|
||||
public SVGPath createIcon() {
|
||||
var path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(getPath());
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Node createIcon(SVGPath path, double size) {
|
||||
if (size == DEFAULT_SIZE)
|
||||
return path;
|
||||
else {
|
||||
double scale = size / DEFAULT_SIZE;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
return new Group(path);
|
||||
}
|
||||
public SVGContainer createIcon(double size) {
|
||||
return new SVGContainer(this, size);
|
||||
}
|
||||
|
||||
public Node createIcon() {
|
||||
return createIcon(DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
public Node createIcon(double size) {
|
||||
return createIcon(createSVGPath(), size);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath p = createSVGPath();
|
||||
p.fillProperty().bind(color);
|
||||
return createIcon(p, DEFAULT_SIZE);
|
||||
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath path = createIcon();
|
||||
path.fillProperty().bind(color);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
175
HMCL/src/main/java/org/jackhuang/hmcl/ui/SVGContainer.java
Normal file
175
HMCL/src/main/java/org/jackhuang/hmcl/ui/SVGContainer.java
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// A lightweight wrapper for displaying [SVG] icons.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class SVGContainer extends Parent {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "svg-container";
|
||||
|
||||
private final SVGPath path = new SVGPath();
|
||||
private SVG icon = SVG.NONE;
|
||||
private double iconSize = SVG.DEFAULT_SIZE;
|
||||
private SVGPath tempPath;
|
||||
private Timeline timeline;
|
||||
|
||||
{
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.path.getStyleClass().add("svg");
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the default icon and the default icon size.
|
||||
public SVGContainer() {
|
||||
this(SVG.NONE, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer showing the given icon using the default icon size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
public SVGContainer(SVG icon) {
|
||||
this(icon, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with a custom icon size. The initial icon is
|
||||
/// [SVG#NONE].
|
||||
///
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(double iconSize) {
|
||||
this(SVG.NONE, iconSize);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the specified icon and size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(SVG icon, double iconSize) {
|
||||
setIconSizeImpl(iconSize);
|
||||
setIcon(icon);
|
||||
}
|
||||
|
||||
public double getIconSize() {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
private void setIconSizeImpl(double newSize) {
|
||||
this.iconSize = newSize;
|
||||
SVG.setSize(path, newSize);
|
||||
if (tempPath != null)
|
||||
SVG.setSize(tempPath, newSize);
|
||||
}
|
||||
|
||||
public void setIconSize(double newSize) {
|
||||
setIconSizeImpl(newSize);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/// Gets the currently displayed icon.
|
||||
public SVG getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/// Sets the icon to display without animation.
|
||||
public void setIcon(SVG newIcon) {
|
||||
setIcon(newIcon, Duration.ZERO);
|
||||
}
|
||||
|
||||
/// Sets the icon to display with a cross-fade animation.
|
||||
public void setIcon(SVG newIcon, Duration animationDuration) {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline = null;
|
||||
}
|
||||
|
||||
SVG oldIcon = this.icon;
|
||||
this.icon = newIcon;
|
||||
|
||||
if (animationDuration.equals(Duration.ZERO)) {
|
||||
path.setContent(newIcon.getPath());
|
||||
path.setOpacity(1);
|
||||
if (getChildren().size() != 1)
|
||||
getChildren().setAll(path);
|
||||
} else {
|
||||
if (tempPath == null) {
|
||||
tempPath = new SVGPath();
|
||||
tempPath.getStyleClass().add("svg");
|
||||
SVG.setSize(tempPath, iconSize);
|
||||
} else
|
||||
tempPath.setOpacity(1);
|
||||
|
||||
tempPath.setContent(oldIcon.getPath());
|
||||
getChildren().setAll(path, tempPath);
|
||||
|
||||
path.setOpacity(0);
|
||||
path.setContent(newIcon.getPath());
|
||||
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
|
||||
),
|
||||
new KeyFrame(animationDuration,
|
||||
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
|
||||
)
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
getChildren().setAll(path);
|
||||
timeline = null;
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Parent
|
||||
|
||||
@Override
|
||||
public double prefWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double prefHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -100,27 +99,17 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(item);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
|
||||
SVG unselectedGraphic, SVG selectedGraphic) {
|
||||
AdvancedListItem item = createNavigationDrawerItem(title, null);
|
||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||
item.setOnAction(e -> tabHeader.select(tab));
|
||||
|
||||
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
||||
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
||||
|
||||
TransitionPane leftGraphic = new TransitionPane();
|
||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||
leftGraphic.setMouseTransparent(true);
|
||||
leftGraphic.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
|
||||
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||
leftGraphic.setPadding(Insets.EMPTY);
|
||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
||||
FXUtils.onChange(item.activeProperty(), active ->
|
||||
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
|
||||
|
||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
|
||||
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
|
||||
item.setLeftGraphic(leftGraphic);
|
||||
return add(item);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
private final HBox right;
|
||||
|
||||
private final ImageView iconImageView;
|
||||
private final SVGPath iconSVG;
|
||||
private final StackPane iconSVGWrapper;
|
||||
private final SVGContainer iconSVGView;
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
this.iconImageView = new ImageView();
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
|
||||
this.iconSVG = new SVGPath();
|
||||
iconSVG.getStyleClass().add("svg");
|
||||
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
|
||||
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
|
||||
|
||||
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
|
||||
iconSVGWrapper.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(iconSVGWrapper, 32);
|
||||
FXUtils.setLimitHeight(iconSVGWrapper, 32);
|
||||
this.iconSVGView = new SVGContainer(32);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER);
|
||||
root.setLeft(left);
|
||||
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVG.setContent(item.getIcon().getPath());
|
||||
left.getChildren().setAll(iconSVGWrapper);
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
|
||||
Reference in New Issue
Block a user