创建 SVGContainer 控件 (#5464)
This commit is contained in:
@@ -18,8 +18,6 @@
|
|||||||
package org.jackhuang.hmcl.ui;
|
package org.jackhuang.hmcl.ui;
|
||||||
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.paint.Paint;
|
import javafx.scene.paint.Paint;
|
||||||
import javafx.scene.shape.SVGPath;
|
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.
|
/// 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`.
|
/// The view boxes of all icons are normalized to `0 0 24 24`.
|
||||||
public enum SVG {
|
public enum SVG {
|
||||||
|
NONE(""), // Empty Icon
|
||||||
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
|
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"),
|
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
|
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;
|
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 final String rawPath;
|
||||||
private String path;
|
private String path;
|
||||||
|
|
||||||
@@ -144,35 +149,20 @@ public enum SVG {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SVGPath createSVGPath() {
|
public SVGPath createIcon() {
|
||||||
var p = new SVGPath();
|
var path = new SVGPath();
|
||||||
p.setContent(getPath());
|
path.getStyleClass().add("svg");
|
||||||
p.getStyleClass().add("svg");
|
path.setContent(getPath());
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Node createIcon(SVGPath path, double size) {
|
|
||||||
if (size == DEFAULT_SIZE)
|
|
||||||
return path;
|
return path;
|
||||||
else {
|
|
||||||
double scale = size / DEFAULT_SIZE;
|
|
||||||
path.setScaleX(scale);
|
|
||||||
path.setScaleY(scale);
|
|
||||||
return new Group(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node createIcon() {
|
public SVGContainer createIcon(double size) {
|
||||||
return createIcon(DEFAULT_SIZE);
|
return new SVGContainer(this, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node createIcon(double size) {
|
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
|
||||||
return createIcon(createSVGPath(), size);
|
SVGPath path = createIcon();
|
||||||
}
|
path.fillProperty().bind(color);
|
||||||
|
return path;
|
||||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
|
||||||
SVGPath p = createSVGPath();
|
|
||||||
p.fillProperty().bind(color);
|
|
||||||
return createIcon(p, DEFAULT_SIZE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
package org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -100,27 +99,17 @@ public class AdvancedListBox extends ScrollPane {
|
|||||||
return add(item);
|
return add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
|
||||||
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
|
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
|
||||||
SVG unselectedGraphic, SVG selectedGraphic) {
|
SVG unselectedGraphic, SVG selectedGraphic) {
|
||||||
AdvancedListItem item = createNavigationDrawerItem(title, null);
|
AdvancedListItem item = createNavigationDrawerItem(title, null);
|
||||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||||
item.setOnAction(e -> tabHeader.select(tab));
|
item.setOnAction(e -> tabHeader.select(tab));
|
||||||
|
|
||||||
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||||
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
|
|
||||||
|
|
||||||
TransitionPane leftGraphic = new TransitionPane();
|
|
||||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
|
||||||
leftGraphic.setMouseTransparent(true);
|
leftGraphic.setMouseTransparent(true);
|
||||||
leftGraphic.setAlignment(Pos.CENTER);
|
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||||
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
|
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
|
||||||
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
|
||||||
leftGraphic.setPadding(Insets.EMPTY);
|
|
||||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
|
||||||
FXUtils.onChange(item.activeProperty(), active ->
|
|
||||||
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
|
|
||||||
|
|
||||||
item.setLeftGraphic(leftGraphic);
|
item.setLeftGraphic(leftGraphic);
|
||||||
return add(item);
|
return add(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
|
|||||||
import com.jfoenix.controls.JFXListView;
|
import com.jfoenix.controls.JFXListView;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.shape.SVGPath;
|
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
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 HBox right;
|
||||||
|
|
||||||
private final ImageView iconImageView;
|
private final ImageView iconImageView;
|
||||||
private final SVGPath iconSVG;
|
private final SVGContainer iconSVGView;
|
||||||
private final StackPane iconSVGWrapper;
|
|
||||||
|
|
||||||
private final Tooltip tooltip = new Tooltip();
|
private final Tooltip tooltip = new Tooltip();
|
||||||
|
|
||||||
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
|||||||
this.iconImageView = new ImageView();
|
this.iconImageView = new ImageView();
|
||||||
FXUtils.limitSize(iconImageView, 32, 32);
|
FXUtils.limitSize(iconImageView, 32, 32);
|
||||||
|
|
||||||
this.iconSVG = new SVGPath();
|
this.iconSVGView = new SVGContainer(32);
|
||||||
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);
|
|
||||||
|
|
||||||
BorderPane.setAlignment(left, Pos.CENTER);
|
BorderPane.setAlignment(left, Pos.CENTER);
|
||||||
root.setLeft(left);
|
root.setLeft(left);
|
||||||
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
|||||||
iconImageView.setImage(fileItem.getImage());
|
iconImageView.setImage(fileItem.getImage());
|
||||||
left.getChildren().setAll(iconImageView);
|
left.getChildren().setAll(iconImageView);
|
||||||
} else {
|
} else {
|
||||||
iconSVG.setContent(item.getIcon().getPath());
|
iconSVGView.setIcon(item.getIcon());
|
||||||
left.getChildren().setAll(iconSVGWrapper);
|
left.getChildren().setAll(iconSVGView);
|
||||||
}
|
}
|
||||||
|
|
||||||
center.setTitle(item.getName());
|
center.setTitle(item.getName());
|
||||||
|
|||||||
Reference in New Issue
Block a user