Compare commits

..
255 changed files with 4295 additions and 12654 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
code_review:
pull_request_opened:
code_review: false
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
+15 -11
View File
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
@@ -27,24 +27,28 @@ jobs:
run: ./gradlew build --no-daemon --parallel
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload JAR
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-jar
path: HMCL/build/libs/HMCL-*.jar
archive: false
path: |
HMCL/build/libs/HMCL-*.jar
HMCL/build/libs/HMCL-*.jar.sha256
- name: Upload EXE
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-exe
path: HMCL/build/libs/HMCL-*.exe
archive: false
path: |
HMCL/build/libs/HMCL-*.exe
HMCL/build/libs/HMCL-*.exe.sha256
- name: Upload SH
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: HMCL/build/libs/HMCL-*.sh
archive: false
path: |
HMCL/build/libs/HMCL-*.sh
HMCL/build/libs/HMCL-*.sh.sha256
-1
View File
@@ -15,7 +15,6 @@ hs_err_pid*
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
terracotta-log-*
/.java/
/.local/
/.cache/
-6
View File
@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Hello Minecraft! Launcher&#10;Copyright (C) &amp;#36;today.year huangyuhui &lt;huanghongxun2008@126.com&gt; and contributors&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;." />
<option name="myName" value="HMCL" />
</copyright>
</component>
-8
View File
@@ -1,8 +0,0 @@
<component name="CopyrightManager">
<settings default="HMCL">
<LanguageOptions name="JAVA">
<option name="fileTypeOverride" value="3" />
<option name="addBlankAfter" value="false" />
</LanguageOptions>
</settings>
</component>
-4
View File
@@ -57,7 +57,6 @@ dependencies {
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
@@ -67,7 +66,6 @@ dependencies {
}
embedResources(libs.authlib.injector)
embedResources(libs.lwjgl.unsafe.agent)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -131,7 +129,6 @@ val addOpens = listOf(
"javafx.base/javafx.beans.property",
"javafx.graphics/javafx.css",
"javafx.graphics/javafx.stage",
"javafx.graphics/javafx.scene",
"javafx.graphics/com.sun.glass.ui",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.javafx.util",
@@ -157,7 +154,6 @@ val hmclProperties = buildList {
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
@@ -1,169 +0,0 @@
/*
* 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.JFXCheckBoxSkin;
import javafx.css.*;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXCheckBox is the material design implementation of a checkbox.
/// it shows ripple effect and a custom selection animation.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXCheckBox extends CheckBox {
public JFXCheckBox(String text) {
super(text);
initialize();
}
public JFXCheckBox() {
initialize();
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXCheckBoxSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-check-box'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-check-box";
/// checkbox color property when selected
private StyleableObjectProperty<Paint> checkedColor;
private static final Color DEFAULT_CHECKED_COLOR = Color.valueOf("#0F9D58");
public StyleableObjectProperty<Paint> checkedColorProperty() {
if (checkedColor == null) {
checkedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.CHECKED_COLOR,
JFXCheckBox.this,
"checkedColor",
DEFAULT_CHECKED_COLOR);
}
return this.checkedColor;
}
public Paint getCheckedColor() {
return checkedColor == null ? DEFAULT_CHECKED_COLOR : checkedColor.get();
}
public void setCheckedColor(Paint color) {
this.checkedColor.set(color);
}
/**
* checkbox color property when not selected
*/
private StyleableObjectProperty<Paint> unCheckedColor;
private static final Color DEFAULT_UNCHECKED_COLOR = Color.valueOf("#5A5A5A");
public StyleableObjectProperty<Paint> unCheckedColorProperty() {
if (unCheckedColor == null) {
unCheckedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNCHECKED_COLOR,
JFXCheckBox.this,
"unCheckedColor",
DEFAULT_UNCHECKED_COLOR);
}
return this.unCheckedColor;
}
public Paint getUnCheckedColor() {
return unCheckedColor == null ? DEFAULT_UNCHECKED_COLOR : unCheckedColor.get();
}
public void setUnCheckedColor(Paint color) {
this.unCheckedColor.set(color);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXCheckBox, Paint> CHECKED_COLOR =
new CssMetaData<>("-jfx-checked-color",
PaintConverter.getInstance(), DEFAULT_CHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.checkedColor == null || !control.checkedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.checkedColorProperty();
}
};
private static final CssMetaData<JFXCheckBox, Paint> UNCHECKED_COLOR =
new CssMetaData<>("-jfx-unchecked-color",
PaintConverter.getInstance(), DEFAULT_UNCHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.unCheckedColor == null || !control.unCheckedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.unCheckedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(CheckBox.getClassCssMetaData());
Collections.addAll(styleables,
CHECKED_COLOR,
UNCHECKED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -23,13 +23,21 @@ import com.jfoenix.controls.events.JFXDialogEvent;
import com.jfoenix.converters.DialogTransitionConverter;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.*;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
@@ -41,6 +49,7 @@ import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
@@ -146,6 +155,7 @@ public class JFXDialog extends StackPane {
});
contentHolder = new StackPane();
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
JFXDepthManager.setDepth(contentHolder, 4);
contentHolder.setPickOnBounds(false);
// ensure stackpane is never resized beyond it's preferred size
@@ -371,14 +381,6 @@ public class JFXDialog extends StackPane {
setCycleDuration(Duration.seconds(0.4));
setDelay(Duration.ZERO);
}
@Override
protected void starting() {
}
@Override
protected void stopping() {
}
}
/***************************************************************************
@@ -1,104 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.controls;
import java.util.List;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class JFXDialogLayout extends StackPane {
private final StackPane heading = new StackPane();
private final StackPane body = new StackPane();
private final FlowPane actions = new FlowPane() {
protected double computeMinWidth(double height) {
if (this.getContentBias() == Orientation.HORIZONTAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minWidth(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getLeft() + this.snapSizeX(maxPref) + insets.getRight();
} else {
return this.computePrefWidth(height);
}
}
protected double computeMinHeight(double width) {
if (this.getContentBias() == Orientation.VERTICAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minHeight(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getTop() + this.snapSizeY(maxPref) + insets.getBottom();
} else {
return this.computePrefHeight(width);
}
}
};
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog-layout";
public JFXDialogLayout() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
VBox layout = new VBox();
this.heading.getStyleClass().add("jfx-layout-heading");
this.heading.getStyleClass().add("title");
this.body.getStyleClass().add("jfx-layout-body");
this.body.prefHeightProperty().bind(this.prefHeightProperty());
this.body.prefWidthProperty().bind(this.prefWidthProperty());
this.actions.getStyleClass().add("jfx-layout-actions");
layout.getChildren().setAll(this.heading, this.body, this.actions);
this.getChildren().add(layout);
}
public ObservableList<Node> getHeading() {
return this.heading.getChildren();
}
public void setHeading(Node... titleContent) {
this.heading.getChildren().setAll(titleContent);
}
public ObservableList<Node> getBody() {
return this.body.getChildren();
}
public void setBody(Node... body) {
this.body.getChildren().setAll(body);
}
public ObservableList<Node> getActions() {
return this.actions.getChildren();
}
public void setActions(Node... actions) {
this.actions.getChildren().setAll(actions);
}
public void setActions(List<? extends Node> actions) {
this.actions.getChildren().setAll(actions);
}
}
@@ -1,271 +0,0 @@
/*
* 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.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.Set;
/// material design implementation of ListCell
///
/// By default, JFXListCell will try to create a graphic node for the cell,
/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method.
///
/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to
/// show the ripple effect upon clicking , to change this behavior you can override the
/// method {[#makeChildrenTransparent()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListCell<T> extends ListCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
protected Node cellContent;
private Rectangle clip;
private Timeline gapAnimation;
private boolean playExpandAnimation = false;
private boolean selectionChanged = false;
/**
* {@inheritDoc}
*/
public JFXListCell() {
initialize();
initListeners();
}
/**
* init listeners to update the vertical gap / selection animation
*/
private void initListeners() {
listViewProperty().addListener((listObj, oldList, newList) -> {
if (newList instanceof JFXListView<?> listView) {
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
cellRippler.rippler.setClip(null);
if (newVal.doubleValue() != 0) {
playExpandAnimation = true;
getListView().requestLayout();
} else {
// fake expand state
double gap = clip.getY() * 2;
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
-gap / 2 - (gap * (getIndex())),
Interpolator.EASE_BOTH)
));
gapAnimation.play();
gapAnimation.setOnFinished((finish) -> {
requestLayout();
Platform.runLater(() -> getListView().requestLayout());
});
}
});
selectedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
selectionChanged = true;
}
});
}
});
}
@Override
protected void layoutChildren() {
super.layoutChildren();
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
double gap = getGap();
if (clip == null) {
clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap);
setClip(clip);
} else {
if (gap != 0) {
if (playExpandAnimation || selectionChanged) {
// fake list collapse state
if (playExpandAnimation) {
this.setTranslateY(-gap / 2 + (-gap * (getIndex())));
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
gapAnimation = new Timeline(new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
0,
Interpolator.EASE_BOTH)));
playExpandAnimation = false;
} else if (selectionChanged) {
clip.setY(0);
clip.setHeight(getHeight());
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH),
new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH)
));
}
playExpandAnimation = false;
selectionChanged = false;
gapAnimation.play();
} else {
if (gapAnimation != null) {
gapAnimation.stop();
}
this.setTranslateY(0);
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
}
} else {
this.setTranslateY(0);
clip.setY(0);
clip.setHeight(getHeight());
}
clip.setX(0);
clip.setWidth(getWidth());
}
if (!getChildren().contains(cellRippler)) {
makeChildrenTransparent();
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
}
}
/**
* this method is used to set some nodes in cell content as mouse transparent nodes
* so clicking on them will trigger the ripple effect.
*/
protected void makeChildrenTransparent() {
for (Node child : getChildren()) {
if (child instanceof Label) {
Set<Node> texts = child.lookupAll("Text");
for (Node text : texts) {
text.setMouseTransparent(true);
}
} else if (child instanceof Shape) {
child.setMouseTransparent(true);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
// remove empty (Trailing cells)
setMouseTransparent(true);
setStyle("-fx-background-color:TRANSPARENT;");
} else {
setMouseTransparent(false);
setStyle(null);
if (item instanceof Node newNode) {
setText(null);
Node currentNode = getGraphic();
if (currentNode == null || !currentNode.equals(newNode)) {
cellContent = newNode;
cellRippler.rippler.cacheRippleClip(false);
// build the Cell node
// RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties
if (newNode instanceof JFXRippler newRippler) {
// build cell container from exisiting rippler
cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty());
cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty());
cellRippler.positionProperty().bind(newRippler.positionProperty());
cellContent = newRippler.getControl();
}
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
setGraphic(cellContent);
}
} else {
setText(item == null ? "null" : item.toString());
setGraphic(null);
}
// show cell tooltip if it's toggled in JFXListView
if (getListView() instanceof JFXListView<?> listView && listView.isShowTooltip()) {
if (item instanceof Label label) {
setTooltip(new Tooltip(label.getText()));
} else if (getText() != null) {
setTooltip(new Tooltip(getText()));
}
}
}
}
// Stylesheet Handling *
/**
* Initialize the style class to 'jfx-list-cell'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.setPadding(new Insets(8, 12, 8, 12));
}
@Override
protected double computePrefHeight(double width) {
double gap = getGap();
return super.computePrefHeight(width) + gap;
}
private double getGap() {
return (getListView() instanceof JFXListView<?> listView)
? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0)
: 0;
}
}
@@ -1,258 +0,0 @@
/*
* 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.JFXListViewSkin;
import javafx.beans.property.*;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import java.util.*;
/// Material design implementation of List View
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListView<T> extends ListView<T> {
/**
* {@inheritDoc}
*/
public JFXListView() {
this.setCellFactory(listView -> new JFXListCell<>());
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXListViewSkin<>(this);
}
private IntegerProperty depth;
public IntegerProperty depthProperty() {
if (depth == null) {
depth = new SimpleIntegerProperty(this, "depth", 0);
}
return depth;
}
public int getDepth() {
return depth != null ? depth.get() : 0;
}
public void setDepth(int depth) {
depthProperty().set(depth);
}
private DoubleProperty currentVerticalGap;
DoubleProperty currentVerticalGapProperty() {
if (currentVerticalGap == null) {
currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap");
}
return currentVerticalGap;
}
private void updateVerticalGap() {
if (isExpanded()) {
currentVerticalGapProperty().set(verticalGap.get());
} else {
currentVerticalGapProperty().set(0);
}
}
/*
* this only works if the items were labels / strings
*/
private BooleanProperty showTooltip;
public final BooleanProperty showTooltipProperty() {
if (showTooltip == null) {
showTooltip = new SimpleBooleanProperty(this, "showTooltip", false);
}
return this.showTooltip;
}
public final boolean isShowTooltip() {
return showTooltip != null && showTooltip.get();
}
public final void setShowTooltip(final boolean showTooltip) {
this.showTooltipProperty().set(showTooltip);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-list-view'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-list-view";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list)
*/
public void propagateMouseEventsToParent() {
this.addEventHandler(MouseEvent.ANY, e -> {
e.consume();
this.getParent().fireEvent(e);
});
}
private StyleableDoubleProperty verticalGap;
public StyleableDoubleProperty verticalGapProperty() {
if (this.verticalGap == null) {
this.verticalGap = new StyleableDoubleProperty(0.0) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "verticalGap";
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.VERTICAL_GAP;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.verticalGap;
}
public Double getVerticalGap() {
return verticalGap == null ? 0.0 : verticalGap.get();
}
public void setVerticalGap(Double gap) {
verticalGapProperty().set(gap);
}
private StyleableBooleanProperty expanded;
public StyleableBooleanProperty expandedProperty() {
if (expanded == null) {
expanded = new StyleableBooleanProperty(false) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "expanded";
}
@Override
public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
return StyleableProperties.EXPANDED;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.expanded;
}
public Boolean isExpanded() {
return expanded != null && expanded.get();
}
public void setExpanded(Boolean expanded) {
expandedProperty().set(expanded);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP =
new CssMetaData<>("-jfx-vertical-gap",
SizeConverter.getInstance(), 0) {
@Override
public boolean isSettable(JFXListView<?> control) {
return control.verticalGap == null || !control.verticalGap.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) {
return control.verticalGapProperty();
}
};
private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED =
new CssMetaData<>("-jfx-expanded",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXListView<?> control) {
// it's only settable if the List is not shown yet
return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound());
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) {
return control.expandedProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ListView.getClassCssMetaData());
Collections.addAll(styleables, VERTICAL_GAP, EXPANDED);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,66 +0,0 @@
/*
* 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.JFXProgressBarSkin;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Skin;
/// JFXProgressBar is the material design implementation of a progress bar.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXProgressBar extends ProgressBar {
/// Initialize the style class to 'jfx-progress-bar'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar";
public JFXProgressBar() {
initialize();
}
public JFXProgressBar(double progress) {
super(progress);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXProgressBarSkin(this);
}
private boolean smoothProgress = true;
public boolean isSmoothProgress() {
return smoothProgress;
}
public void setSmoothProgress(boolean smoothProgress) {
this.smoothProgress = smoothProgress;
}
private void initialize() {
setPrefWidth(200);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -1,160 +0,0 @@
/*
* 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.JFXRadioButtonSkin;
import javafx.css.*;
import javafx.css.converter.ColorConverter;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXRadioButton is the material design implementation of a radio button.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXRadioButton extends RadioButton {
public JFXRadioButton(String text) {
super(text);
initialize();
}
public JFXRadioButton() {
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXRadioButtonSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/// Initialize the style class to 'jfx-radio-button'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-radio-button";
/// default color used when the radio button is selected
private StyleableObjectProperty<Color> selectedColor;
private static final Color DEFAULT_SELECTED_COLOR = Color.valueOf("#0F9D58");
public final StyleableObjectProperty<Color> selectedColorProperty() {
if (selectedColor == null) {
selectedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.SELECTED_COLOR,
JFXRadioButton.this,
"selectedColor",
DEFAULT_SELECTED_COLOR);
}
return this.selectedColor;
}
public final Color getSelectedColor() {
return selectedColor == null ? DEFAULT_SELECTED_COLOR : this.selectedColorProperty().get();
}
public final void setSelectedColor(final Color selectedColor) {
this.selectedColorProperty().set(selectedColor);
}
/// default color used when the radio button is not selected
private StyleableObjectProperty<Color> unSelectedColor;
private static final Color DEFAULT_UNSELECTED_COLOR = Color.valueOf("#5A5A5A");
public final StyleableObjectProperty<Color> unSelectedColorProperty() {
if (unSelectedColor == null) {
unSelectedColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.UNSELECTED_COLOR,
JFXRadioButton.this,
"unSelectedColor",
DEFAULT_UNSELECTED_COLOR);
}
return this.unSelectedColor;
}
public final Color getUnSelectedColor() {
return unSelectedColor == null ? DEFAULT_UNSELECTED_COLOR : this.unSelectedColorProperty().get();
}
public final void setUnSelectedColor(final Color unSelectedColor) {
this.unSelectedColorProperty().set(unSelectedColor);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXRadioButton, Color> SELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-selected-color",
ColorConverter.getInstance(), DEFAULT_SELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.selectedColor == null || !control.selectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.selectedColorProperty();
}
};
private static final CssMetaData<JFXRadioButton, Color> UNSELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-unselected-color",
ColorConverter.getInstance(), DEFAULT_UNSELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.unSelectedColor == null || !control.unSelectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.unSelectedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(RadioButton.getClassCssMetaData());
Collections.addAll(styleables,
SELECTED_COLOR,
UNSELECTED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -231,11 +231,10 @@ public class JFXRippler extends StackPane {
*
* @return the ripple radius size
*/
protected double computeRippleRadius(double centerX, double centerY) {
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
protected double computeRippleRadius() {
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
}
protected void setOverLayBounds(Rectangle overlay) {
@@ -474,7 +473,7 @@ public class JFXRippler extends StackPane {
super(centerX,
centerY,
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
computeRippleRadius() : getRipplerRadius(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
@@ -519,7 +518,7 @@ public class JFXRippler extends StackPane {
new KeyValue(opacityProperty(),
1,
RIPPLE_INTERPOLATOR)
), new KeyFrame(Duration.millis(600), inKeyValues));
), new KeyFrame(Duration.millis(900), inKeyValues));
setScaleX(0);
setScaleY(0);
@@ -1,180 +0,0 @@
/*
* 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.converters.IndicatorPositionConverter;
import com.jfoenix.skins.JFXSliderSkin;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.scene.control.Skin;
import javafx.scene.control.Slider;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXSlider is the material design implementation of a slider.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSlider extends Slider {
public JFXSlider() {
super(0, 100, 50);
initialize();
}
public JFXSlider(double min, double max, double value) {
super(min, max, value);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSliderSkin(this);
}
private void initialize() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public enum IndicatorPosition {
LEFT, RIGHT
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/// String binding factory for the slider value.
/// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number).
///
///
/// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)):
/// ```java
/// JFXSlider mySlider = ...
/// mySlider.setValueFactory(slider ->
/// Bindings.createStringBinding(
/// () -> ((int) slider.getValue()) + "%",
/// slider.valueProperty()
/// )
/// );
/// ```
///
/// NOTE: might be replaced later with a call back to create the animated thumb node
///
/// @param callback a callback to create the string value binding
private ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactory;
public final ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactoryProperty() {
if (valueFactory == null) {
valueFactory = new SimpleObjectProperty<>(this, "valueFactory");
}
return valueFactory;
}
/// @return the current slider value factory
public final Callback<JFXSlider, StringBinding> getValueFactory() {
return valueFactory == null ? null : valueFactory.get();
}
/// sets custom string binding for the slider text value
///
/// @param valueFactory a callback to create the string value binding
public final void setValueFactory(final Callback<JFXSlider, StringBinding> valueFactory) {
this.valueFactoryProperty().set(valueFactory);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-slider'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-slider";
/// indicates the position of the slider indicator, can be
/// either LEFT or RIGHT
private StyleableObjectProperty<IndicatorPosition> indicatorPosition;
public StyleableObjectProperty<IndicatorPosition> indicatorPositionProperty() {
if (indicatorPosition == null) {
indicatorPosition = new SimpleStyleableObjectProperty<>(
StyleableProperties.INDICATOR_POSITION,
JFXSlider.this,
"indicatorPosition",
IndicatorPosition.LEFT);
}
return this.indicatorPosition;
}
public IndicatorPosition getIndicatorPosition() {
return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get();
}
public void setIndicatorPosition(IndicatorPosition pos) {
indicatorPositionProperty().set(pos);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSlider, IndicatorPosition> INDICATOR_POSITION = new CssMetaData<>(
"-jfx-indicator-position",
IndicatorPositionConverter.getInstance(),
IndicatorPosition.LEFT) {
@Override
public boolean isSettable(JFXSlider control) {
return control.indicatorPosition == null || !control.indicatorPosition.isBound();
}
@Override
public StyleableProperty<IndicatorPosition> getStyleableProperty(JFXSlider control) {
return control.indicatorPositionProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
Slider.getClassCssMetaData());
Collections.addAll(styleables, INDICATOR_POSITION);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,138 +0,0 @@
/*
* 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.JFXSpinnerSkin;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Skin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/// JFXSpinner is the material design implementation of a loading spinner.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSpinner extends ProgressIndicator {
public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS;
public JFXSpinner() {
this(INDETERMINATE_PROGRESS);
}
public JFXSpinner(double progress) {
super(progress);
init();
}
private void init() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSpinnerSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-spinner'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
private static final double DEFAULT_RADIUS = 16.0;
/**
* specifies the radius of the spinner node, by default it's set to `16.0`
*/
private StyleableDoubleProperty radius;
public final StyleableDoubleProperty radiusProperty() {
if (this.radius == null) {
this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS,
JFXSpinner.this,
"radius",
DEFAULT_RADIUS);
}
return this.radius;
}
public final double getRadius() {
return radius != null ? radius.get() : DEFAULT_RADIUS;
}
public final void setRadius(final double radius) {
this.radiusProperty().set(radius);
}
public double getStartingAngle() {
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSpinner, Number> RADIUS =
new CssMetaData<>("-jfx-radius",
SizeConverter.getInstance(), DEFAULT_RADIUS) {
@Override
public boolean isSettable(JFXSpinner control) {
return control.radius == null || !control.radius.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
return control.radiusProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
Collections.addAll(styleables, RADIUS);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -88,14 +88,6 @@ public class JFXTextField extends TextField {
useJFXContextMenu(this);
}
// https://github.com/HMCL-dev/HMCL/issues/5822
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
@Override
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
/***************************************************************************
* *
* Properties *
@@ -1,146 +0,0 @@
/*
* 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.utils.JFXNodeUtils;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import java.lang.ref.WeakReference;
/// JFXTreeCell is simple material design implementation of a tree cell.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeCell<T> extends TreeCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXTreeCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
private HBox hbox;
private final StackPane selectedPane = new StackPane();
private final InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
isEmpty());
private final WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
treeItemGraphicInvalidationListener);
private WeakReference<TreeItem<T>> treeItemRef;
public JFXTreeCell() {
selectedPane.getStyleClass().add("selection-bar");
selectedPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY)));
selectedPane.setPrefWidth(3);
selectedPane.setMouseTransparent(true);
selectedProperty().addListener((o, oldVal, newVal) -> selectedPane.setVisible(newVal ? true : false));
final InvalidationListener treeItemInvalidationListener = observable -> {
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
if (oldTreeItem != null) {
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
}
TreeItem<T> newTreeItem = getTreeItem();
if (newTreeItem != null) {
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
treeItemRef = new WeakReference<>(newTreeItem);
}
};
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
treeItemProperty().addListener(weakTreeItemListener);
if (getTreeItem() != null) {
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
}
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (!getChildren().contains(selectedPane)) {
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
getChildren().add(0, selectedPane);
}
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
cellRippler.releaseRipple();
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
selectedPane.setVisible(isSelected());
}
private void updateDisplay(T item, boolean empty) {
if (item == null || empty) {
hbox = null;
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
if (treeItem != null && treeItem.getGraphic() != null) {
if (item instanceof Node) {
setText(null);
if (hbox == null) {
hbox = new HBox(3);
}
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
setGraphic(hbox);
} else {
hbox = null;
setText(item.toString());
setGraphic(treeItem.getGraphic());
}
} else {
hbox = null;
if (item instanceof Node) {
setText(null);
setGraphic((Node) item);
} else {
setText(item.toString());
setGraphic(null);
}
}
}
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateDisplay(item, empty);
setMouseTransparent(item == null || empty);
}
}
@@ -1,48 +0,0 @@
/*
* 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 javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
/// JFXTreeView is the material design implementation of a TreeView
/// with expand/collapse animation and selection indicator.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeView<T> extends TreeView<T> {
private static final String DEFAULT_STYLE_CLASS = "jfx-tree-view";
public JFXTreeView() {
init();
}
public JFXTreeView(TreeItem<T> root) {
super(root);
init();
}
private void init() {
this.setCellFactory((view) -> new JFXTreeCell<>());
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -1,80 +0,0 @@
/*
* 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.datamodels.treetable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeTableColumn;
/// data model that is used in JFXTreeTableView, it's used to implement
/// the grouping feature.
///
/// **Note:** the data object used in JFXTreeTableView **must** extends this class
///
/// @param <T> is the concrete object of the Tree table
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class RecursiveTreeObject<T> {
/// grouped children objects
private ObservableList<T> children = FXCollections.observableArrayList();
public ObservableList<T> getChildren() {
return children;
}
public void setChildren(ObservableList<T> children) {
this.children = children;
}
/// Whether or not the object is grouped by a specified tree table column
ObjectProperty<TreeTableColumn<T, ?>> groupedColumn = new SimpleObjectProperty<>();
public final ObjectProperty<TreeTableColumn<T, ?>> groupedColumnProperty() {
return this.groupedColumn;
}
public final TreeTableColumn<T, ?> getGroupedColumn() {
return this.groupedColumnProperty().get();
}
public final void setGroupedColumn(final TreeTableColumn<T, ?> groupedColumn) {
this.groupedColumnProperty().set(groupedColumn);
}
/// the value that must be shown when grouped
ObjectProperty<Object> groupedValue = new SimpleObjectProperty<>();
public final ObjectProperty<Object> groupedValueProperty() {
return this.groupedValue;
}
public final Object getGroupedValue() {
return this.groupedValueProperty().get();
}
public final void setGroupedValue(final Object groupedValue) {
this.groupedValueProperty().set(groupedValue);
}
}
@@ -1,55 +0,0 @@
/*
* 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.events;
import javafx.event.Event;
import javafx.event.EventType;
import java.io.Serial;
/// JFXDialog events, used exclusively by the following methods:
///
/// - [com.jfoenix.controls.JFXDialog#show()]
/// - [com.jfoenix.controls.JFXDialog#close()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXDialogEvent extends Event {
@Serial
private static final long serialVersionUID = 1L;
/// This event occurs when a JFXDialog is closed, no longer visible to the user
/// ( after the exit animation ends )
public static final EventType<JFXDialogEvent> CLOSED = new EventType<>(Event.ANY, "JFX_DIALOG_CLOSED");
/// This event occurs when a JFXDialog is opened, visible to the user
/// ( after the entrance animation ends )
public static final EventType<JFXDialogEvent> OPENED = new EventType<>(Event.ANY, "JFX_DIALOG_OPENED");
/// Construct a new JFXDialog `Event` with the specified event type
///
/// @param eventType the event type
public JFXDialogEvent(EventType<? extends Event> eventType) {
super(eventType);
}
}
@@ -42,11 +42,11 @@ public final class JFXDepthManager {
private static final DropShadow[] depth = new DropShadow[] {
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 0),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 8, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 12, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 16, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 8)
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 1),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 6, 0, 0, 2),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 10, 0, 0, 3),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 14, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 6)
};
/**
@@ -40,16 +40,12 @@ import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Shadi Shaheen
*/
@@ -82,7 +78,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
// create JFX Decorator
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
pickerDecorator.setOnCloseButtonAction(this::close);
pickerDecorator.setOnCloseButtonAction(this::updateColor);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
@@ -157,22 +153,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
paraTransition.play();
});
container.getChildren().add(tabs);
HBox actionsHBox = new HBox();
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(event -> updateColor());
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(event -> close());
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
container.getChildren().add(actionsHBox);
initRun = () -> {
// change tabs labels font color according to the selected color
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
@@ -199,7 +179,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String rgb = String.format("rgb(%d, %d, %d)",
String rgb = String.format("rgba(%d, %d, %d, 1)",
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
@@ -252,13 +232,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
return ColorScheme.newBuilder(Themes.getColorScheme())
.setPrimaryColorSeed(fill)
.build()
.getPrimary();
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
.bind(Bindings.createObjectBinding(() -> {
@@ -288,6 +262,9 @@ public class JFXCustomColorPickerDialog extends StackPane {
});
};
container.getChildren().add(tabs);
this.getChildren().add(container);
this.setPadding(new Insets(0));
@@ -317,11 +294,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
if (!systemChange) {
userChange = true;
try {
Color color = Color.valueOf(colorWebString);
if (color.getOpacity() != 1.0) {
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
}
curvedColorPicker.setColor(color);
curvedColorPicker.setColor(Color.valueOf(colorWebString));
} catch (IllegalArgumentException ignored) {
// if color is not valid then do nothing
}
@@ -336,9 +309,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
public void setCurrentColor(Color currentColor) {
this.currentColorProperty.set(currentColor);
if (curvedColorPicker != null && currentColor != null) {
curvedColorPicker.setColor(currentColor);
}
}
Color getCurrentColor() {
@@ -32,7 +32,8 @@ public class JFXListViewSkin<T> extends ListViewSkin<T> {
public JFXListViewSkin(final JFXListView<T> listView) {
super(listView);
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue()));
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
FXUtils.smoothScrolling(flow);
@@ -1,225 +0,0 @@
/*
* 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.skins;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.*;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
/// # Material Design ProgressBar Skin
///
/// @author Shadi Shaheen
/// @version 2.0
/// @since 2017-10-06
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
private static final double DEFAULT_HEIGHT = 4;
private final StackPane track;
private final StackPane bar;
private final Rectangle clip;
private Animation transition;
private final TreeShowingProperty treeShowingProperty;
private double fullWidth;
public JFXProgressBarSkin(JFXProgressBar control) {
super(control);
this.treeShowingProperty = new TreeShowingProperty(control);
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
track = new StackPane();
track.getStyleClass().setAll("track");
bar = new StackPane();
bar.getStyleClass().setAll("bar");
clip = new Rectangle();
clip.setArcWidth(DEFAULT_HEIGHT);
clip.setArcHeight(DEFAULT_HEIGHT);
bar.setClip(clip);
getChildren().setAll(track, bar);
getSkinnable().requestLayout();
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + DEFAULT_HEIGHT + bottomInset;
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height);
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
track.resizeRelocate(x, y, w, h);
bar.resizeRelocate(x, y, w, h);
clip.relocate(0, 0);
clip.setWidth(0);
clip.setHeight(h);
clip.setTranslateX(0);
fullWidth = w;
clearAnimation();
updateProgress(false);
}
private boolean wasIndeterminate = false;
private void updateProgress(boolean playProgressAnimation) {
double progress = Math.min(getSkinnable().getProgress(), 1.0);
boolean isIndeterminate = progress < 0.0;
boolean isTreeShowing = treeShowingProperty.get();
if (isIndeterminate != wasIndeterminate) {
wasIndeterminate = isIndeterminate;
clearAnimation();
clip.setTranslateX(0);
}
if (isIndeterminate) { // indeterminate
if (isTreeShowing) {
if (transition == null) {
transition = createIndeterminateTransition();
transition.playFromStart();
} else {
transition.play();
}
} else if (transition != null) {
transition.pause();
}
} else { // determinate
clearAnimation();
if (isTreeShowing && playProgressAnimation
&& AnimationUtils.isAnimationEnabled()
&& getSkinnable().isSmoothProgress()) {
transition = createDeterminateTransition(progress);
transition.playFromStart();
} else {
clip.setWidth(computeBarWidth(progress));
}
}
}
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
private Transition createIndeterminateTransition() {
double minWidth = 0;
double maxWidth = fullWidth * 0.4;
Transition transition = new Transition() {
{
setInterpolator(Interpolator.LINEAR);
setCycleDuration(INDETERMINATE_DURATION);
}
@Override
protected void interpolate(double frac) {
double currentWidth;
if (frac <= 0.5) {
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
} else {
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
}
double targetCenter;
if (frac <= 0.1) {
targetCenter = 0.0;
} else if (frac >= 0.9) {
targetCenter = fullWidth;
} else {
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
}
clip.setWidth(currentWidth);
clip.setTranslateX(targetCenter - currentWidth / 2.0);
}
};
transition.setCycleCount(Timeline.INDEFINITE);
return transition;
}
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
private Timeline createDeterminateTransition(double targetProgress) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
new KeyFrame(DETERMINATE_DURATION,
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
);
timeline.setOnFinished(e -> {
if (transition == timeline) {
transition = null;
}
});
return timeline;
}
private void clearAnimation() {
if (transition != null) {
transition.stop();
transition = null;
}
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
clearAnimation();
}
private double computeBarWidth(double progress) {
assert progress >= 0 && progress <= 1;
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
}
}
@@ -1,291 +0,0 @@
/*
* 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.skins;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXSlider.IndicatorPosition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Slider;
import javafx.scene.control.skin.SliderSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.util.Duration;
/// # Material Design Slider Skin
///
/// rework of JFXSliderSkin by extending Java SliderSkin
/// this solves padding and resizing issues
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSliderSkin extends SliderSkin {
private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min");
private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max");
private final Pane mouseHandlerPane = new Pane();
private final Text sliderValue;
private final StackPane coloredTrack;
private final StackPane thumb;
private final StackPane track;
private final StackPane animatedThumb;
private NumberAxis tickLine;
private Timeline timeline;
private double indicatorRotation;
private double horizontalRotation;
private double shifting;
public JFXSliderSkin(JFXSlider slider) {
super(slider);
track = (StackPane) getSkinnable().lookup(".track");
thumb = (StackPane) getSkinnable().lookup(".thumb");
tickLine = (NumberAxis) getSkinnable().lookup(".axis");
if (tickLine != null) tickLine.setAnimated(false);
coloredTrack = new StackPane();
coloredTrack.getStyleClass().add("colored-track");
coloredTrack.setMouseTransparent(true);
sliderValue = new Text();
sliderValue.getStyleClass().setAll("slider-value");
animatedThumb = new StackPane();
animatedThumb.getStyleClass().add("animated-thumb");
animatedThumb.getChildren().add(sliderValue);
animatedThumb.setMouseTransparent(true);
animatedThumb.setScaleX(0);
animatedThumb.setScaleY(0);
thumb.layoutXProperty().addListener(x -> {
if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL);
});
thumb.layoutYProperty().addListener(y -> {
if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL);
});
addJFXChildren();
getChildren().addListener((ListChangeListener<Node>) c -> {
while (c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(added -> {
if (added instanceof NumberAxis) {
tickLine = (NumberAxis) added;
tickLine.setAnimated(false);
}
});
}
}
});
registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren());
registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren());
registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding());
initListeners();
}
private void addJFXChildren() {
ObservableList<Node> children = getChildren();
Slider slider = getSkinnable();
if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) {
children.add(0, tickLine);
}
if (children.contains(coloredTrack)) return;
children.add(children.indexOf(thumb), coloredTrack);
children.add(children.indexOf(thumb), animatedThumb);
children.add(0, mouseHandlerPane);
}
private void refreshSliderValueBinding() {
sliderValue.textProperty().unbind();
if (((JFXSlider) getSkinnable()).getValueFactory() != null) {
sliderValue.textProperty()
.bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable()));
} else {
sliderValue.textProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().getLabelFormatter() != null) {
return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue());
} else {
return String.valueOf(Math.round(getSkinnable().getValue()));
}
}, getSkinnable().valueProperty()));
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (timeline == null) {
initAnimation(getSkinnable().getOrientation());
}
double prefWidth = animatedThumb.prefWidth(-1);
animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth));
boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
double width, height, layoutX, layoutY;
if (horizontal) {
width = thumb.getLayoutX() - snappedLeftInset();
height = track.getHeight();
layoutX = track.getLayoutX();
layoutY = track.getLayoutY();
animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2);
} else {
height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
width = track.getWidth();
layoutX = track.getLayoutX();
layoutY = thumb.getLayoutY();
animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2);
}
coloredTrack.resizeRelocate(layoutX, layoutY, width, height);
mouseHandlerPane.resizeRelocate(x, y, w, h);
}
private void initializeVariables() {
shifting = 30 + thumb.getWidth();
if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) {
horizontalRotation = -90;
}
if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) {
indicatorRotation = 180;
shifting = -shifting;
}
final double rotationAngle = 45;
sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation);
animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation);
}
private void initListeners() {
// delegate slider mouse events to track node
mouseHandlerPane.setOnMousePressed(this::delegateToTrack);
mouseHandlerPane.setOnMouseReleased(this::delegateToTrack);
mouseHandlerPane.setOnMouseDragged(this::delegateToTrack);
// animate value node
track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
refreshSliderValueBinding();
updateValueStyleClass();
getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass());
}
private void delegateToTrack(MouseEvent event) {
if (!event.isConsumed()) {
event.consume();
track.fireEvent(event);
}
}
private void updateValueStyleClass() {
getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue());
getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue());
}
private void initAnimation(Orientation orientation) {
initializeVariables();
double thumbPos, thumbNewPos;
DoubleProperty layoutProperty;
if (orientation == Orientation.HORIZONTAL) {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutY() - thumb.getHeight();
thumbNewPos = thumbPos - shifting;
} else {
double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1));
thumbPos = thumb.getLayoutY() - height / 2;
thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight();
}
layoutProperty = animatedThumb.translateYProperty();
} else {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutX() - thumb.getWidth();
thumbNewPos = thumbPos - shifting;
} else {
double width = animatedThumb.prefWidth(-1);
thumbPos = thumb.getLayoutX() - width / 2;
thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth();
}
layoutProperty = animatedThumb.translateXProperty();
}
clearAnimation();
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)),
new KeyFrame(
Duration.seconds(0.2),
new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH)));
}
@Override
public void dispose() {
super.dispose();
clearAnimation();
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline.getKeyFrames().clear();
timeline = null;
}
}
}
@@ -1,264 +0,0 @@
/*
* 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.skins;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Group;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import java.util.ArrayList;
import java.util.List;
/// JFXSpinner material design skin
///
/// @author Shadi Shaheen & Gerard Moubarak
/// @version 1.0
/// @since 2017-09-25
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
private static final double DEFAULT_STROKE_WIDTH = 4;
private JFXSpinner control;
private final TreeShowingProperty treeShowingProperty;
private boolean isValid = false;
private Timeline timeline;
private Arc arc;
private Arc track;
private final StackPane arcPane;
private final Rectangle fillRect;
private final double startingAngle;
public JFXSpinnerSkin(JFXSpinner control) {
super(control);
this.control = control;
this.treeShowingProperty = new TreeShowingProperty(control);
this.startingAngle = control.getStartingAngle();
arc = new Arc();
arc.setManaged(false);
arc.setLength(180);
arc.getStyleClass().setAll("arc");
arc.setFill(Color.TRANSPARENT);
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
track = new Arc();
track.setManaged(false);
track.setLength(360);
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
track.getStyleClass().setAll("track");
track.setFill(Color.TRANSPARENT);
fillRect = new Rectangle();
fillRect.setFill(Color.TRANSPARENT);
final Group group = new Group(fillRect, track, arc);
group.setManaged(false);
arcPane = new StackPane(group);
arcPane.setPrefSize(50, 50);
getChildren().setAll(arcPane);
// register listeners
registerChangeListener(control.progressProperty(), obs -> updateProgress());
registerChangeListener(treeShowingProperty, obs -> updateProgress());
}
private void updateProgress() {
double progress = Double.min(getSkinnable().getProgress(), 1.0);
if (progress < 0) { // indeterminate
boolean treeShowing = treeShowingProperty.get();
if (treeShowing) {
if (timeline == null) {
timeline = createTransition();
timeline.playFromStart();
} else {
timeline.play();
}
} else if (timeline != null) {
timeline.pause();
}
} else { // determinate
clearAnimation();
arc.setStartAngle(90);
arc.setLength(-360 * progress);
}
}
private double computeSize() {
return control.getRadius() * 2 + arc.getStrokeWidth() * 2;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefWidth(-1);
} else {
return computeSize();
}
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefHeight(-1);
} else {
return computeSize();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
final double strokeWidth = arc.getStrokeWidth();
double radius = control.getRadius();
final double arcSize = snapSizeX(radius * 2 + strokeWidth);
arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
updateArcLayout(radius, arcSize);
fillRect.setWidth(arcSize);
fillRect.setHeight(arcSize);
if (!isValid) {
updateProgress();
isValid = true;
}
}
private void updateArcLayout(double radius, double arcSize) {
arc.setRadiusX(radius);
arc.setRadiusY(radius);
arc.setCenterX(arcSize / 2);
arc.setCenterY(arcSize / 2);
track.setRadiusX(radius);
track.setRadiusY(radius);
track.setCenterX(arcSize / 2);
track.setCenterY(arcSize / 2);
track.setStrokeWidth(arc.getStrokeWidth());
}
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
frames.add(new KeyFrame(Duration.seconds(duration),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 45 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 90 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 135 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 435 + startingAngle,
Interpolator.LINEAR)));
}
private Timeline createTransition() {
Timeline timeline;
if (AnimationUtils.isAnimationEnabled()) {
var keyFrames = new ArrayList<KeyFrame>(17);
addKeyFrames(keyFrames, 0, 0);
addKeyFrames(keyFrames, 450, 1.4);
addKeyFrames(keyFrames, 900, 2.8);
addKeyFrames(keyFrames, 1350, 4.2);
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
1845 + startingAngle,
Interpolator.LINEAR)));
timeline = new Timeline();
timeline.getKeyFrames().setAll(keyFrames);
} else {
final double arcLength = 250;
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
new KeyFrame(Duration.seconds(1.2),
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
);
}
timeline.setCycleCount(Timeline.INDEFINITE);
return timeline;
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline = null;
}
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
clearAnimation();
arc = null;
track = null;
control = null;
}
}
@@ -19,25 +19,13 @@
package com.jfoenix.utils;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Shadi Shaheen
/// @version 1.0
@@ -70,70 +58,6 @@ public final class JFXNodeUtils {
}
}
private static final @NotNull Function<Node, ObservableBooleanValue> treeVisiblePropertyGetter = initTreeVisiblePropertyGetter();
private static @NotNull Function<Node, ObservableBooleanValue> initTreeVisiblePropertyGetter() {
MethodHandles.Lookup lookup;
try {
lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
} catch (IllegalAccessException e) {
LOG.warning("Failed to get private lookup for Node", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
try {
Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty");
if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) {
LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType());
return JFXNodeUtils::defaultTreeVisibleProperty;
}
MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod)
.asType(MethodType.methodType(ObservableBooleanValue.class, Node.class));
return item -> {
try {
return (ObservableBooleanValue) handle.invokeExact((Node) item);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new AssertionError("Unreachable", e);
}
};
} catch (Exception e) {
LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
}
/// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback
private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) {
return item.visibleProperty();
}
public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) {
return treeVisiblePropertyGetter.apply(item);
}
public static boolean isTreeVisible(Node item) {
return treeVisibleProperty(item).getValue();
}
public static boolean isTreeShowing(Node node) {
if (node == null)
return false;
Scene scene = node.getScene();
if (scene == null)
return false;
Window window = scene.getWindow();
if (window == null || !window.isShowing())
return false;
return isTreeVisible(node);
}
private JFXNodeUtils() {
}
@@ -1,143 +0,0 @@
/*
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jfoenix.utils;
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Window;
/**
* Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing
* it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and
* the scene must have a {@link Window} which is currently showing.<p>
*
* This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in
* an observable form.
*/
public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase {
private final ChangeListener<Boolean> windowShowingChangedListener = (obs, old, current) -> updateTreeShowing();
private final ChangeListener<Window> sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current);
private final ChangeListener<Scene> nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current);
private final Node node;
private final ObservableBooleanValue treeVisibleProperty;
private boolean valid;
private boolean treeShowing;
/**
* Constructs a new instance.
*
* @param node a {@link Node} for which the tree showing status should be observed, cannot be null
*/
public TreeShowingProperty(Node node) {
this.node = node;
this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node);
this.node.sceneProperty().addListener(nodeSceneChangedListener);
this.treeVisibleProperty.addListener(windowShowingChangedListener);
sceneChanged(null, node.getScene());
}
@Override
public Object getBean() {
return node;
}
@Override
public String getName() {
return "treeShowing";
}
/**
* Cleans up any listeners that this class may have registered on the {@link Node}
* that was supplied at construction.
*/
public void dispose() {
node.sceneProperty().removeListener(nodeSceneChangedListener);
if (treeVisibleProperty != null)
treeVisibleProperty.removeListener(windowShowingChangedListener);
valid = false; // prevents unregistration from triggering an invalidation notification
sceneChanged(node.getScene(), null);
}
protected void invalidate() {
if (valid) {
valid = false;
fireValueChangedEvent();
}
}
@Override
public boolean get() {
if (!valid) {
updateTreeShowing();
valid = true;
}
return treeShowing;
}
private void sceneChanged(Scene oldScene, Scene newScene) {
if (oldScene != null) {
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
}
if (newScene != null) {
newScene.windowProperty().addListener(sceneWindowChangedListener);
}
windowChanged(
oldScene == null ? null : oldScene.getWindow(),
newScene == null ? null : newScene.getWindow()
);
}
private void windowChanged(Window oldWindow, Window newWindow) {
if (oldWindow != null) {
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
}
if (newWindow != null) {
newWindow.showingProperty().addListener(windowShowingChangedListener);
}
updateTreeShowing();
}
private void updateTreeShowing() {
boolean newValue = JFXNodeUtils.isTreeShowing(node);
if (newValue != treeShowing) {
treeShowing = newValue;
invalidate();
}
}
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -50,8 +49,6 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
@@ -203,6 +200,9 @@ public final class EntryPoint {
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
@@ -223,20 +223,6 @@ public final class EntryPoint {
}
}
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
@@ -17,36 +17,35 @@
*/
package org.jackhuang.hmcl;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.io.IOException;
@@ -66,8 +65,8 @@ import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@@ -112,7 +111,7 @@ public final class Launcher extends Application {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
} else {
checkConfigInTempDir();
@@ -183,27 +182,6 @@ public final class Launcher extends Application {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
okButton.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(okButton.textProperty(), i18n("button.ok")),
new KeyValue(okButton.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
alert.setOnShown(e -> timeline.play());
alert.setOnCloseRequest(e -> timeline.stop());
return alert.showAndWait().orElse(null) == ButtonType.YES;
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
@@ -243,7 +221,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
EntryPoint.exit(0);
}
}
@@ -49,7 +49,6 @@ public final class Metadata {
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
@@ -1,20 +1,3 @@
/*
* 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.countly;
import org.jackhuang.hmcl.Metadata;
@@ -21,16 +21,12 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
@@ -154,60 +150,4 @@ public final class HMCLGameLauncher extends DefaultLauncher {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@Override
protected void appendJvmArgs(CommandBuilder result) {
super.appendJvmArgs(result);
if (config().getAllowAutoAgent()
&& !options.isNoGeneratedJVMArgs()
&& !options.isNoGeneratedOptimizingJVMArgs()
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
try {
result.add("-javaagent:" + extractLwjglUnsafeAgent());
} catch (Exception e) {
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
}
}
}
private Path extractLwjglUnsafeAgent() throws IOException {
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
if (agentVersion == null) {
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
}
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
String fileName = library.getArtifact().getFileName();
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
if (agentPath.toString().contains("=")) {
throw new IOException("Invalid library path: " + agentPath);
}
byte[] bytes;
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
if (input == null) {
throw new IOException("/assets/" + fileName + " not found");
}
bytes = input.readAllBytes();
}
if (Files.isRegularFile(agentPath)) {
try {
if (Files.size(agentPath) == bytes.length) {
return agentPath;
}
} catch (IOException e) {
LOG.warning("Failed to check size of " + agentPath, e);
}
}
Files.createDirectories(agentPath.getParent());
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
return agentPath;
}
}
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
return FXUtils.loadImage(iconFile.get());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -65,7 +65,6 @@ import java.util.stream.Collectors;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
@@ -76,8 +75,6 @@ import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
private final Profile profile;
private Account account;
private final String selectedVersion;
@@ -200,28 +197,6 @@ public final class LauncherHelper {
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> {
if (config().getAllowAutoAgent()
|| setting.isNoJVMArgs()
|| setting.isNoOptimizingJVMArgs()
|| Boolean.TRUE.equals(config().getShownTips().get(LWJGL_3_4_1_TIP))
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
return Task.completed(null);
} else {
CompletableFuture<Void> future = new CompletableFuture<>();
runInFX(() -> {
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
config().setAllowAutoAgent(true);
future.complete(null);
}, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
future.complete(null);
});
});
return Task.fromCompletableFuture(future);
}
})
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
@@ -274,11 +249,11 @@ public final class LauncherHelper {
i18n("message.doing"),
() -> launchingLatch.getCount() == 0, 6.95
).withStage("launch.state.waiting_launching"))
.withStagesHints(
new Task.StagesHint("launch.state.java"),
new Task.StagesHint("launch.state.dependencies", List.of("hmcl.install.assets", "hmcl.install.libraries", "hmcl.modpack.download")),
new Task.StagesHint("launch.state.logging_in"),
new Task.StagesHint("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.java",
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@@ -412,16 +387,7 @@ public final class LauncherHelper {
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = null;
if (gameVersion.compareTo("1.12.2") == 0) {
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
if (cleanroomVersion.isPresent()) {
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
}
}
if (minimumJavaVersion == null)
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
Controllers.dialog(
@@ -436,17 +402,8 @@ public final class LauncherHelper {
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else {
if (gameVersion.compareTo("1.12.2") == 0) {
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
if (cleanroomVersion.isPresent()) {
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
}
}
if (targetJavaVersion == null)
targetJavaVersion = version.getJavaVersion();
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
@@ -476,7 +433,7 @@ public final class LauncherHelper {
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
@@ -511,14 +468,9 @@ public final class LauncherHelper {
return result;
} else {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
.orElse("");
gameJavaVersion = !cleanroomVersion.isEmpty()
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
: GameJavaVersion.JAVA_21;
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
gameJavaVersion = GameJavaVersion.JAVA_21;
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
@@ -608,14 +560,9 @@ public final class LauncherHelper {
case MODDED_JAVA_21:
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
break;
case CLEANROOM: {
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
if (!cleanroomVersion.isEmpty())
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
else
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
case CLEANROOM_JAVA_21:
suggestions.add(i18n("launch.advice.cleanroom"));
break;
}
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
@@ -115,8 +115,8 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
}
@Override
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
return getBackedRemoteModRepository().getModById(downloadProvider, id);
public RemoteMod getModById(String id) throws IOException {
return getBackedRemoteModRepository().getModById(id);
}
@Override
@@ -125,7 +125,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}
@@ -51,10 +51,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -170,7 +167,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
}
public static boolean isExternalGameNameConflicts(String name) {
@@ -214,24 +211,23 @@ public final class ModpackHelper {
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
@@ -245,11 +241,9 @@ public final class ModpackHelper {
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -163,13 +163,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
@Override
public void close() {
if (!future.isDone())
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
stop();
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
@@ -99,7 +99,7 @@ public final class HMCLJavaRepository implements JavaRepository {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
for (Path file : stream) {
try {
@@ -117,7 +117,8 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
list.add(executable);
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
} catch (Throwable e) {
@@ -130,23 +131,28 @@ public final class HMCLJavaRepository implements JavaRepository {
}
@Override
public Collection<Path> getAllJava(Platform platform) {
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<Path> list = new ArrayList<>();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
getAllJava(list, platform, platformRoot);
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
@@ -159,16 +165,16 @@ public final class HMCLJavaRepository implements JavaRepository {
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.fromExecutable(executable);
info = JavaInfoUtils.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download().version().name(), null);
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.component());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles().getFiles().forEach((path, file) -> {
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -28,6 +27,7 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -39,10 +39,35 @@ public final class JavaInfoUtils {
private JavaInfoUtils() {
}
public static @NotNull JavaInfo fromExecutable(Path executable) throws IOException {
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
@@ -69,6 +94,7 @@ public final class JavaInfoUtils {
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
@@ -77,9 +103,14 @@ public final class JavaInfoUtils {
}
}
@JsonSerializable
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
@SerializedName("java.version") String javaVersion,
@SerializedName("java.vendor") String javaVendor) {
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
}
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
@@ -32,9 +30,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
@@ -44,14 +40,9 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -64,16 +55,6 @@ public final class JavaManager {
private JavaManager() {
}
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
"Java",
"BellSoft",
"AdoptOpenJDK",
"Zulu",
"Microsoft",
"Eclipse Foundation",
"Semeru"
};
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
@@ -179,12 +160,12 @@ public final class JavaManager {
return javaRuntime;
}
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
@@ -323,7 +304,7 @@ public final class JavaManager {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
@@ -345,7 +326,7 @@ public final class JavaManager {
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
@@ -353,56 +334,56 @@ public final class JavaManager {
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
if (useCache)
searcher.loadCache();
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
}
break;
case LINUX:
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case MACOS:
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
searcher.tryAddJavaExecutable(Path.of("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searcher.searchAllJavaInDirectory(dir);
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
@@ -416,16 +397,16 @@ public final class JavaManager {
// Search Minecraft bundled runtimes
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
@@ -439,7 +420,7 @@ public final class JavaManager {
}
try {
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -449,17 +430,16 @@ public final class JavaManager {
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
for (String path : paths) {
try {
searcher.tryAddJavaHome(Path.of(path));
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
searcher.tryAddJavaExecutable(Path.of(javaPath));
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
@@ -467,404 +447,233 @@ public final class JavaManager {
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
searcher.addResult(currentJava.getBinary(), currentJava);
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
searcher.saveCache();
LOG.trace(searcher.javaRuntimes.values().stream().sorted()
LOG.trace(javaRuntimes.values().stream().sorted()
.map(it -> String.format(" - %s %s (%s, %s): %s",
it.isJDK() ? "JDK" : "JRE",
it.getVersion(),
it.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
it.getBinary()))
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
return searcher.javaRuntimes;
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
}
private static final class Searcher {
private final Path cacheFile;
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
private final Set<Path> failed = new HashSet<>();
private boolean needRefreshCache = false;
Searcher(Path cacheFile) {
this.cacheFile = cacheFile;
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
private static final int CACHE_MAJOR_VERSION = 0;
private static final int CACHE_MINOR_VERSION = 0;
private record JavaInfoCache(String key, JavaInfo info) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
}
void loadCache() {
if (Files.notExists(cacheFile))
return;
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
JsonElement fileVersion = jsonFile.get("version");
Matcher matcher;
if (fileVersion instanceof JsonPrimitive version
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
int major = Integer.parseInt(matcher.group("major"));
String minorString = matcher.group("minor");
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
} else
throw new IOException("Invalid version JSON: " + fileVersion);
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
for (JsonElement element : cachesArray) {
try {
var obj = (JsonObject) element;
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
String key = obj.getAsJsonPrimitive("key").getAsString();
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
infoBuilder.setVendor(vendor.getAsString());
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
} catch (Exception e) {
LOG.warning("Invalid cache: " + element);
needRefreshCache = true;
}
}
} catch (Exception ex) {
LOG.warning("Failed to load cache file: " + cacheFile);
needRefreshCache = true;
}
}
void saveCache() {
if (!needRefreshCache)
return;
needRefreshCache = false;
try {
FileUtils.saveSafely(cacheFile, output -> {
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
writer.beginObject();
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
writer.name("caches");
writer.beginArray();
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
Path path = entry.getKey();
JavaInfoCache cache = entry.getValue();
JavaInfo info = cache.info();
writer.beginObject();
writer.name("path").value(path.toString());
writer.name("key").value(cache.key());
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
writer.name("java.version").value(info.getVersion());
if (info.getVendor() != null)
writer.name("java.vendor").value(info.getVendor());
writer.endObject();
}
writer.endArray();
writer.endObject();
}
});
} catch (Exception e) {
LOG.warning("Failed to save cache file: " + cacheFile);
}
}
private static @Nullable String createCacheKey(Path realPath) {
Path binDir = realPath.getParent();
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
return null;
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
return null;
Path javaHome = binDir.getParent();
if (javaHome == null)
return null;
Path libDir = javaHome.resolve("lib");
if (!Files.isDirectory(libDir))
return null;
BasicFileAttributes launcherAttributes;
String releaseHash = null;
BasicFileAttributes coreLibsAttributes = null;
try {
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
} else {
Path coreLibsFile = libDir.resolve("rt.jar");
if (!Files.isRegularFile(coreLibsFile)) {
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
if (!Files.isRegularFile(coreLibsFile))
return null;
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
}
}
} catch (Exception e) {
LOG.warning("Failed to create cache key for " + realPath, e);
return null;
}
StringJoiner joiner = new StringJoiner(",");
joiner.add("sz:" + launcherAttributes.size());
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
if (releaseHash != null)
joiner.add(releaseHash);
if (coreLibsAttributes != null) {
joiner.add("rsz:" + coreLibsAttributes.size());
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
}
return joiner.toString();
}
void addResult(Path realPath, JavaRuntime javaRuntime) {
javaRuntimes.put(realPath, javaRuntime);
}
void tryAddJavaHome(Path javaHome) {
tryAddJavaExecutable(getExecutable(javaHome));
}
void tryAddJavaExecutable(Path executable) {
tryAddJavaExecutable(executable, false);
}
void tryAddJavaExecutable(Path executable, boolean isManaged) {
try {
executable = executable.toRealPath();
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
return;
LOG.warning("Failed to read release file " + releaseFile, e);
}
}
if (javaRuntimes.containsKey(executable)
|| failed.contains(executable)
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
String cacheKey = createCacheKey(executable);
if (cacheKey != null) {
JavaInfoCache cache = caches.get(executable);
if (cache != null) {
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
return;
} else {
caches.remove(executable);
needRefreshCache = true;
}
}
} else if (caches.remove(executable) != null) {
needRefreshCache = true;
}
JavaInfo info;
if (info == null) {
try {
info = JavaInfoUtils.fromExecutable(executable);
info = JavaInfoUtils.fromExecutable(executable, false);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
failed.add(executable);
return;
}
if (cacheKey != null) {
caches.put(executable, new JavaInfoCache(cacheKey, info));
needRefreshCache = true;
}
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
}
void tryAddJavaInComponentDir(String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(dir);
}
void searchAllJavaInRepository(Platform platform) {
for (Path java : REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
if (platform.os() == OperatingSystem.MACOS) {
// In the past, we used 'osx' as the checked name for macOS
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
searchAllJavaInDirectory(platformRoot);
}
}
void searchAllOfficialJava(Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
void searchAllOfficialJava(Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(platform, component, verify);
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfoUtils.fromExecutable(executable, true);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
void searchAllJavaInDirectory(Path directory) {
if (!Files.isDirectory(directory)) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
return;
}
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir);
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
void searchJavaInProgramFiles(String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Path.of(programFiles);
} catch (InvalidPathException ignored) {
return;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
searchAllJavaInDirectory(path.resolve(vendor));
}
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Paths.get(programFiles);
} catch (InvalidPathException ignored) {
return;
}
void searchJavaInMacJavaVirtualMachines(Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
// ==== Windows Registry Support ====
void queryJavaInRegistryKey(WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
try {
tryAddJavaHome(Path.of(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
@@ -222,36 +222,6 @@ public final class Config extends ObservableSetting {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
@SerializedName("disableAutoShowUpdateDialog")
private final BooleanProperty disableAutoShowUpdateDialog = new SimpleBooleanProperty(false);
public BooleanProperty disableAutoShowUpdateDialogProperty() {
return disableAutoShowUpdateDialog;
}
public boolean isDisableAutoShowUpdateDialog() {
return disableAutoShowUpdateDialog.get();
}
public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog) {
this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog);
}
@SerializedName("disableAprilFools")
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
public BooleanProperty disableAprilFoolsProperty() {
return disableAprilFools;
}
public boolean isDisableAprilFools() {
return disableAprilFools.get();
}
public void setDisableAprilFools(boolean disableAprilFools) {
this.disableAprilFools.set(disableAprilFools);
}
@SerializedName("shownTips")
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
@@ -567,21 +537,6 @@ public final class Config extends ObservableSetting {
this.versionListSource.set(versionListSource);
}
@SerializedName("defaultAddonSource")
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
public StringProperty defaultAddonSourceProperty() {
return defaultAddonSource;
}
public String getDefaultAddonSource() {
return defaultAddonSource.get();
}
public void setDefaultAddonSource(String defaultAddonSource) {
this.defaultAddonSource.set(defaultAddonSource);
}
@SerializedName("hasProxy")
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
@@ -704,21 +659,6 @@ public final class Config extends ObservableSetting {
this.disableAutoGameOptions.set(disableAutoGameOptions);
}
@SerializedName("allowAutoAgent")
private final BooleanProperty allowAutoAgent = new SimpleBooleanProperty(false);
public BooleanProperty allowAutoAgentProperty() {
return allowAutoAgent;
}
public boolean getAllowAutoAgent() {
return allowAutoAgent.get();
}
public void setAllowAutoAgent(boolean allowAutoAgent) {
this.allowAutoAgent.set(allowAutoAgent);
}
// Accounts
@SerializedName("authlibInjectorServers")
@@ -1,20 +1,3 @@
/*
* 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.setting;
public final class SambaException extends RuntimeException {
@@ -23,7 +23,6 @@ import com.jfoenix.validation.base.ValidatorBase;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
@@ -65,16 +64,12 @@ import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -87,7 +82,6 @@ public final class Controllers {
public static final String JAVA_VERSION_TIP = "javaVersion";
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
public static final String SOFTWARE_RENDERING = "softwareRendering";
public static final String APRIL_FOOLS = "aprilFools";
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
@@ -105,14 +99,9 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> {
Path file = files.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
Path modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
@@ -236,8 +225,6 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
LOG.info("April Fools: " + AprilFools.isEnabled());
if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
@@ -445,53 +432,6 @@ public final class Controllers {
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
aprilFools:
if (AprilFools.isEnabled()) {
int currentYear = LocalDate.now().getYear();
if (config().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
break aprilFools;
if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
break aprilFools;
SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
.filter(locale -> "lzh".equals(locale.getName()))
.findFirst().orElse(null);
if (lzh == null) {
LOG.warning("No supported locale found for lzh");
break aprilFools;
}
Runnable updateShowTips = () -> config().getShownTips().put(APRIL_FOOLS, currentYear);
Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
MessageType.QUESTION, () -> {
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
LOG.info("Switching locale to " + lzh);
updateShowTips.run();
config().setLocalization(lzh);
Controllers.onApplicationStop();
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
// Ignore
}
try {
Restarter.restartSelf();
} catch (IOException e) {
LOG.warning("Failed to restart self", e);
}
Platform.exit();
}, updateShowTips);
}, updateShowTips);
}
}
public static void dialog(Region content) {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -38,6 +37,7 @@ import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
@@ -51,6 +51,7 @@ import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
@@ -67,7 +68,6 @@ import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
@@ -235,8 +235,8 @@ public final class FXUtils {
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static final List<String> IMAGE_EXTENSIONS = List.of(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
@@ -417,6 +417,14 @@ public final class FXUtils {
return region.getMaxHeight();
}
public static Node limitingSize(Node node, double width, double height) {
StackPane pane = new StackPane(node);
pane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(pane, width);
FXUtils.setLimitHeight(pane, height);
return pane;
}
public static void limitCellWidth(ListView<?> listView, ListCell<?> cell) {
ReadOnlyDoubleProperty widthProperty;
@@ -1241,8 +1249,8 @@ public final class FXUtils {
.setSignificance(Task.TaskSignificance.MINOR);
}
public static Task<Image> getRemoteImageTask(List<URI> uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(uris)
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(uri)
.setSignificance(Task.TaskSignificance.MINOR)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
.setSignificance(Task.TaskSignificance.MINOR);
@@ -1283,30 +1291,6 @@ public final class FXUtils {
return button;
}
public static JFXButton newToggleButton4(SVG icon, int size) {
JFXButton button = new JFXButton();
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(size));
return button;
}
public static void setOnActionWithCooldown(ButtonBase button, Runnable action) {
setOnActionWithCooldown(button, action, Motion.SHORT4);
}
public static void setOnActionWithCooldown(ButtonBase button, Runnable action, Duration cooldown) {
button.setOnAction(e -> {
button.setDisable(true);
var pause = new PauseTransition(cooldown);
pause.setOnFinished(event -> button.setDisable(false));
pause.play();
action.run();
e.consume();
});
}
public static Label newSafeTruncatedLabel() {
Label label = new Label();
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
@@ -274,10 +273,10 @@ public class GameCrashWindow extends Stage {
logWindow.show();
}
private CompletableFuture<Path> exportGameCrashInfo() {
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
return CompletableFuture.supplyAsync(() ->
CompletableFuture.supplyAsync(() ->
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs -> {
long processStartTime = managedProcess.getProcess().info()
@@ -302,7 +301,20 @@ public class GameCrashWindow extends Stage {
return false;
}
});
}).thenApply(ignored -> logFile);
})
.handleAsync((result, exception) -> {
if (exception == null) {
FXUtils.showFileInExplorer(logFile);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
return null;
}, Schedulers.javafx());
}
private final class View extends VBox {
@@ -432,35 +444,8 @@ public class GameCrashWindow extends Stage {
HBox toolBar = new HBox();
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
{
SpinnerPane exportButtonPane = new SpinnerPane();
exportButtonPane.getStyleClass().add("small-spinner-pane");
JFXButton exportButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportButtonPane.setContent(exportButton);
exportButton.setOnAction(e -> {
exportButtonPane.showSpinner();
exportGameCrashInfo().whenCompleteAsync((result, exception) -> {
exportButtonPane.hideSpinner();
if (exception == null) {
FXUtils.showFileInExplorer(result);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.success", result),
i18n("message.success"),
MessageDialogPane.MessageType.SUCCESS
).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
i18n("message.error"),
MessageDialogPane.MessageType.ERROR
).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
}, Schedulers.javafx());
});
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnAction(e -> showLogWindow());
@@ -472,7 +457,7 @@ public class GameCrashWindow extends Stage {
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton);
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
@@ -28,15 +28,16 @@ import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -294,14 +295,14 @@ public class InstallerItem extends Control {
paneWrapper.pseudoClassStateChanged(CARD, control.style == Style.CARD);
if (control.iconType != null) {
var imageContainer = new ImageContainer(32);
imageContainer.setImage(control.iconType.getIcon());
imageContainer.setMouseTransparent(true);
imageContainer.getStyleClass().add("installer-item-image");
pane.getChildren().add(imageContainer);
ImageView view = new ImageView(control.iconType.getIcon());
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
node.getStyleClass().add("installer-item-image");
pane.getChildren().add(node);
if (control.style == Style.CARD) {
VBox.setMargin(imageContainer, new Insets(8, 0, 16, 0));
VBox.setMargin(node, new Insets(8, 0, 16, 0));
}
}
@@ -347,7 +348,9 @@ public class InstallerItem extends Control {
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
JFXButton removeButton = FXUtils.newToggleButton4(SVG.CLOSE);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon());
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
@@ -17,7 +17,10 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
@@ -36,17 +39,12 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.platform.*;
import java.io.IOException;
import java.nio.file.Files;
@@ -242,10 +240,6 @@ public final class LogWindow extends Stage {
});
}
private ManagedProcess getGameProcess() {
return gameProcess;
}
@Override
protected Skin<?> createDefaultSkin() {
return new LogWindowSkin(this);
@@ -263,7 +257,6 @@ public final class LogWindow extends Stage {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final Set<ListCell<Log>> selected = new HashSet<>();
private final JFXSnackbar snackbar = new JFXSnackbar();
LogWindowSkin(LogWindowImpl control) {
super(control);
@@ -272,7 +265,7 @@ public final class LogWindow extends Stage {
vbox.setPadding(new Insets(3, 0, 3, 0));
getSkinnable().stackPane.getChildren().setAll(vbox);
getChildren().setAll(getSkinnable().stackPane);
snackbar.registerSnackbarContainer(getSkinnable().stackPane);
{
BorderPane borderPane = new BorderPane();
@@ -388,8 +381,7 @@ public final class LogWindow extends Stage {
}
}
FXUtils.copyText(stringBuilder.toString(), null);
snackbar.fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(i18n("message.copied"))));
FXUtils.copyText(stringBuilder.toString());
}
});
@@ -416,12 +408,11 @@ public final class LogWindow extends Stage {
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
SpinnerPane exportDumpPane = new SpinnerPane();
exportDumpPane.getStyleClass().add("small-spinner-pane");
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
FXUtils.installFastTooltip(exportDumpPane, i18n("logwindow.export_dump.no_dependency"));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
exportDumpButton.setDisable(true);
}
exportDumpPane.setContent(exportDumpButton);
@@ -430,13 +421,6 @@ public final class LogWindow extends Stage {
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
control.getGameProcess().getProcess()
.onExit()
.thenRunAsync(() -> {
terminateButton.setDisable(true);
exportDumpButton.setDisable(true);
}, Schedulers.javafx());
vbox.getChildren().add(bottom);
}
}
@@ -1,228 +0,0 @@
/*
* 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.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import java.lang.ref.WeakReference;
/// @author Glavo
public final class MemoryStatusBar extends Control {
private static final String DEFAULT_STYLE_CLASS = "memory-status-bar";
public MemoryStatusBar() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
setPrefWidth(200);
}
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
/// The memory status of the system.
///
/// The property will be automatically updated.
///
/// The property has no bean, because it may share between multiple MemoryStatusBar instances.
public ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
return memoryStatus;
}
public PhysicalMemoryStatus getMemoryStatus() {
return memoryStatus.get();
}
private final DoubleProperty memoryAllocated = new SimpleDoubleProperty();
public DoubleProperty memoryAllocatedProperty() {
return memoryAllocated;
}
public double getMemoryAllocated() {
return memoryAllocated.get();
}
public void setMemoryAllocated(double memoryAllocated) {
this.memoryAllocated.set(memoryAllocated);
}
@Override
protected SkinBase<MemoryStatusBar> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<MemoryStatusBar> {
private static final int HEIGHT = 4;
private final StackPane track;
private final Rectangle memoryUsed;
private final Rectangle memoryAllocate;
private double contentWidth = -1;
Skin(MemoryStatusBar control) {
super(control);
track = new StackPane();
track.getStyleClass().add("track");
track.setPadding(new Insets(HEIGHT, 0, 0, 0));
memoryUsed = new Rectangle();
memoryUsed.setManaged(false);
memoryUsed.setArcWidth(HEIGHT);
memoryUsed.setArcHeight(HEIGHT);
memoryUsed.getStyleClass().add("memory-used");
memoryAllocate = new Rectangle();
memoryAllocate.setManaged(false);
memoryAllocate.setArcWidth(HEIGHT);
memoryAllocate.setArcHeight(HEIGHT);
memoryAllocate.getStyleClass().add("memory-allocate");
this.getChildren().setAll(track, memoryAllocate, memoryUsed);
registerInvalidationListener(control.memoryStatusProperty(), it -> updateBar());
registerInvalidationListener(control.memoryAllocatedProperty(), it -> updateBar());
}
private void updateBar() {
if (contentWidth <= 0) {
// not yet layout
return;
}
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
double total = status.getTotal();
double used = status.getUsed();
double allocated = getSkinnable().getMemoryAllocated();
if (total <= 0) {
memoryUsed.setWidth(contentWidth);
memoryAllocate.setWidth(contentWidth);
return;
}
double usedWidth = MathUtils.clamp(contentWidth * (used / total), 0, contentWidth);
double allocatedWidth = MathUtils.clamp(contentWidth * (allocated / total) + usedWidth, 0, contentWidth);
memoryUsed.setWidth(usedWidth);
memoryAllocate.setWidth(allocatedWidth);
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
track.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
memoryUsed.relocate(contentX, contentY);
memoryAllocate.relocate(contentX, contentY);
this.contentWidth = contentWidth;
memoryUsed.setHeight(contentHeight);
memoryAllocate.setHeight(contentHeight);
updateBar();
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return leftInset + track.prefWidth(height) + rightInset;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + HEIGHT + bottomInset;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
}
private static final class UpdateMemoryStatus extends Thread {
private static final int UPDATE_INTERVAL = 3000;
@FXThread
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
@FXThread
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
if (memoryStatusPropertyCache != null) {
var property = memoryStatusPropertyCache.get();
if (property != null) {
return property;
}
}
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
memoryStatusPropertyCache = new WeakReference<>(property);
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
return property;
}
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
setName("UpdateMemoryStatus");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (true) {
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
var memoryStatusProperty = memoryStatusPropertyRef.get();
if (memoryStatusProperty == null)
return;
if (Controllers.isStopped())
return;
Platform.runLater(() -> memoryStatusProperty.set(status));
try {
//noinspection BusyWait
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException e) {
return;
}
}
}
}
}
@@ -125,7 +125,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
@@ -144,11 +144,13 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
})
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh"));
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
JFXButton btnUpload = FXUtils.newToggleButton4(SVG.CHECKROOM);
JFXButton btnUpload = new JFXButton();
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnAction(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
@@ -159,23 +161,29 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
.start();
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.CHECKROOM.createIcon());
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
btnUpload.disableProperty().bind(Bindings.not(skinnable.canUploadSkin()));
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
right.getChildren().add(spinnerUpload);
JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
JFXButton btnCopyUUID = new JFXButton();
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnUpload.getStyleClass().add("toggle-icon4");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.CONTENT_COPY.createIcon());
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
spinnerCopyUUID.setContent(btnCopyUUID);
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.DELETE.createIcon());
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -115,7 +115,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
GridPane.setRowIndex(lblServerName, 1);
lblServerWarning = new Label(i18n("account.injector.http"));
lblServerWarning.setStyle("-fx-text-fill: -monet-error;");
lblServerWarning.setStyle("-fx-text-fill: red;");
GridPane.setColumnIndex(lblServerWarning, 0);
GridPane.setRowIndex(lblServerWarning, 2);
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
@@ -399,7 +399,9 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
});
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = FXUtils.newToggleButton4(SVG.ADD, 20);
JFXButton btnAddServer = new JFXButton();
btnAddServer.setGraphic(SVG.ADD.createIcon(20));
btnAddServer.getStyleClass().add("toggle-icon4");
btnAddServer.setOnAction(e -> {
Controllers.dialog(new AddAuthlibInjectorServerPane());
});
@@ -564,7 +566,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
}
public static class DialogCharacterSelector extends JFXDialogLayout implements CharacterSelector {
private static class DialogCharacterSelector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
@@ -576,17 +578,17 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnAction(e -> latch.countDown());
cancel.getStyleClass().add("dialog-cancel");
listBox.startCategory(i18n("account.choose").toUpperCase(Locale.ROOT));
setBody(listBox);
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setActions(hbox);
setBottom(hbox);
onEscPressed(this, cancel::fire);
}
@@ -1,20 +1,3 @@
/*
* 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.account;
import com.jfoenix.controls.JFXButton;
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
@@ -84,11 +85,13 @@ public class AdvancedListItem extends Control {
}
public void setRightAction(SVG icon, Runnable action) {
var button = FXUtils.newToggleButton4(icon, 14);
var button = new JFXButton();
button.setOnAction(e -> {
action.run();
e.consume();
});
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(14));
setAlignment(button, Pos.CENTER);
setRightGraphic(button);
}
@@ -80,7 +80,8 @@ public class FileSelector extends HBox {
JFXTextField customField = new JFXTextField();
FXUtils.bindString(customField, valueProperty());
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.FOLDER_OPEN.createIcon(15));
selectButton.setOnAction(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
@@ -26,7 +26,7 @@ import javafx.scene.control.ScrollBar;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.Lang;
// Referenced in root.css
@SuppressWarnings("unused")
@@ -75,7 +75,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
*/
if (getSkinnable().getMax() > getSkinnable().getMin()) {
dragStart = thumb.localToParent(me.getX(), me.getY());
double clampedValue = MathUtils.clamp(getSkinnable().getValue(), getSkinnable().getMin(), getSkinnable().getMax());
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
me.consume();
}
@@ -107,7 +107,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(MathUtils.clamp(newValue, getSkinnable().getMin(), getSkinnable().getMax()));
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
}
}
@@ -129,12 +129,12 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
track.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
track.relocate(0, -8);
track.relocate(0, -5);
track.widthProperty().bind(scrollBar.widthProperty());
track.setHeight(8);
track.setHeight(5);
} else {
track.relocate(-8, 0);
track.setWidth(8);
track.relocate(-5, 0);
track.setWidth(5);
track.heightProperty().bind(scrollBar.heightProperty());
}
@@ -144,13 +144,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumb.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -8);
thumb.relocate(0, -5);
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.setHeight(8);
thumb.setHeight(5);
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
} else {
thumb.relocate(-8, 0);
thumb.setWidth(8);
thumb.relocate(-5, 0);
thumb.setWidth(5);
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
}
@@ -162,7 +162,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 5;
}
@Override
@@ -171,7 +171,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 5;
}
};
}
@@ -1,163 +0,0 @@
/*
* 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.construct;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.ArrayList;
import java.util.List;
/// A custom ImageView with fixed size and corner radius support.
public class ImageContainer extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "image-container";
private final ImageView imageView = new ImageView();
private final Rectangle clip = new Rectangle();
public ImageContainer(double size) {
this(size, size);
}
public ImageContainer(double width, double height) {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
FXUtils.setLimitWidth(this, width);
FXUtils.setLimitHeight(this, height);
imageView.setPreserveRatio(true);
FXUtils.limitSize(imageView, width, height);
StackPane.setAlignment(imageView, Pos.CENTER);
clip.setWidth(width);
clip.setHeight(height);
updateCornerRadius(getCornerRadius());
this.setClip(clip);
this.getChildren().setAll(imageView);
}
private void updateCornerRadius(double radius) {
clip.setArcWidth(radius);
clip.setArcHeight(radius);
}
private static final double DEFAULT_CORNER_RADIUS = 6.0;
private StyleableDoubleProperty cornerRadius;
public StyleableDoubleProperty cornerRadiusProperty() {
if (this.cornerRadius == null) {
cornerRadius = new StyleableDoubleProperty() {
@Override
public Object getBean() {
return ImageContainer.this;
}
@Override
public String getName() {
return "cornerRadius";
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.CORNER_RADIUS;
}
@Override
protected void invalidated() {
updateCornerRadius(get());
}
};
}
return cornerRadius;
}
public double getCornerRadius() {
return cornerRadius == null ? DEFAULT_CORNER_RADIUS : cornerRadius.get();
}
public void setCornerRadius(double radius) {
cornerRadiusProperty().set(radius);
}
public ObjectProperty<Image> imageProperty() {
return imageView.imageProperty();
}
public Image getImage() {
return imageView.getImage();
}
public void setImage(Image image) {
imageView.setImage(image);
}
public BooleanProperty smoothProperty() {
return imageView.smoothProperty();
}
public boolean isSmooth() {
return imageView.isSmooth();
}
public void setSmooth(boolean smooth) {
imageView.setSmooth(smooth);
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return StyleableProperties.STYLEABLES;
}
private static final class StyleableProperties {
private static final CssMetaData<ImageContainer, Number> CORNER_RADIUS =
new CssMetaData<>("-jfx-corner-radius", SizeConverter.getInstance(), DEFAULT_CORNER_RADIUS) {
@Override
public boolean isSettable(ImageContainer control) {
return control.cornerRadius == null || !control.cornerRadius.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(ImageContainer control) {
return control.cornerRadiusProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
styleables.add(CORNER_RADIUS);
STYLEABLES = List.copyOf(styleables);
}
}
}
@@ -28,6 +28,7 @@ import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -39,7 +40,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@DefaultProperty("image")
public final class ImagePickerItem extends BorderPane {
private final ImageContainer imageContainer;
private final ImageView imageView;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
@@ -47,20 +48,25 @@ public final class ImagePickerItem extends BorderPane {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
imageContainer = new ImageContainer(32);
imageContainer.setSmooth(false);
imageView = new ImageView();
imageView.setSmooth(false);
imageView.setPreserveRatio(true);
JFXButton selectButton = FXUtils.newToggleButton4(SVG.EDIT, 20);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.EDIT.createIcon(20));
selectButton.onActionProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = FXUtils.newToggleButton4(SVG.RESTORE, 20);
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.RESTORE.createIcon(20));
deleteButton.onActionProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
FXUtils.installFastTooltip(deleteButton, i18n("button.reset"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageContainer, selectButton, deleteButton);
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setSpacing(8);
setRight(hBox);
@@ -72,7 +78,7 @@ public final class ImagePickerItem extends BorderPane {
vBox.setAlignment(Pos.CENTER_LEFT);
setLeft(vBox);
imageContainer.imageProperty().bind(image);
imageView.imageProperty().bind(image);
}
public String getTitle() {
@@ -122,4 +128,8 @@ public final class ImagePickerItem extends BorderPane {
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
}
@@ -30,6 +30,9 @@ import org.jackhuang.hmcl.ui.SVG;
public class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setTrailingIcon(SVG.ARROW_FORWARD);
@@ -47,14 +50,7 @@ public class LineButton extends LineButtonBase {
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
protected int getTrailingTextIndex() {
return IDX_TRAILING;
}
protected int getTrailingIconIndex() {
return getTrailingTextIndex() + 1;
container.setMouseTransparent(true);
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
@@ -114,13 +110,12 @@ public class LineButton extends LineButtonBase {
trailingTextLabel = new Label();
trailingTextLabel.getStyleClass().add("trailing-label");
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
trailingTextLabel.setMouseTransparent(true);
}
trailingTextLabel.setText(message);
setNode(getTrailingTextIndex(), trailingTextLabel);
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
} else if (trailingTextLabel != null) {
trailingTextLabel.setText("");
setNode(getTrailingTextIndex(), null);
setNode(IDX_TRAILING_TEXT, null);
}
}
};
@@ -154,7 +149,7 @@ public class LineButton extends LineButtonBase {
@Override
protected void invalidated() {
setNode(getTrailingIconIndex(), get());
setNode(IDX_TRAILING_ICON, get());
}
};
@@ -176,7 +171,6 @@ public class LineButton extends LineButtonBase {
public void setTrailingIcon(SVG rightIcon, double size) {
Node rightIconNode = rightIcon.createIcon(size);
rightIconNode.getStyleClass().add("trailing-icon");
rightIconNode.setMouseTransparent(true);
setTrailingIcon(rightIconNode);
}
}
@@ -64,7 +64,6 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
this.container = new HBox(SPACING);
container.getStyleClass().add("line-component-container");
container.setAlignment(Pos.CENTER_LEFT);
container.setPickOnBounds(true);
this.titleLabel = new Label();
titleLabel.getStyleClass().add("title-label");
@@ -20,18 +20,16 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -43,7 +41,7 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public class LineSelectButton<T> extends LineButton {
public final class LineSelectButton<T> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
@@ -83,8 +81,6 @@ public class LineSelectButton<T> extends LineButton {
PopupMenu popupMenu = new PopupMenu();
this.popup = new JFXPopup(popupMenu);
popupMenu.addEventHandler(KeyEvent.KEY_PRESSED, this::handleKeyEvent);
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
@@ -217,32 +213,4 @@ public class LineSelectButton<T> extends LineButton {
return items.get();
}
private void handleKeyEvent(KeyEvent event) {
ObservableList<T> list = getItems();
if (list == null || list.isEmpty()) return;
int index = list.indexOf(getValue());
var code = event.getCode();
if (code == KeyCode.UP) {
if (index > 0) {
setValue(list.get(index - 1));
} else {
setValue(list.get(list.size() - 1));
}
event.consume();
} else if (code == KeyCode.DOWN) {
if (index < list.size() - 1) {
setValue(list.get(index + 1));
} else {
setValue(list.get(0));
}
event.consume();
} else if (code == KeyCode.ENTER || code == KeyCode.ESCAPE) {
if (popup != null && popup.isShowing()) {
popup.hide();
event.consume();
}
}
}
}
@@ -189,7 +189,6 @@ public class SpinnerPane extends Control {
Skin(SpinnerPane control) {
super(control);
root.setClip(null);
updateContent();
this.getChildren().setAll(root);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -25,11 +26,12 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
@@ -155,6 +157,8 @@ public class TabHeader extends Control implements TabControl, PageAware {
private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected");
private final Color ripplerColor = Color.valueOf("#FFFF8D");
private final HeaderContainer header;
private boolean isSelectingTab = false;
private Tab<?> selectedTab;
@@ -210,6 +214,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(2);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
@@ -588,6 +593,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
private final Tab<?> tab;
private final Label tabText;
private final BorderPane inner;
private final JFXRippler rippler;
public TabHeaderContainer(Tab<?> tab) {
this.tab = tab;
@@ -598,8 +604,9 @@ public class TabHeader extends Control implements TabControl, PageAware {
inner = new BorderPane();
inner.setCenter(tabText);
inner.getStyleClass().add("tab-container");
inner.setMouseTransparent(true);
RipplerContainer rippler = new RipplerContainer(inner);
rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT);
rippler.getStyleClass().add("tab-rippler");
rippler.setRipplerFill(ripplerColor);
getChildren().setAll(rippler);
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -82,9 +83,8 @@ public class TaskExecutorDialogPane extends BorderPane {
setCancel(cancel);
btnCancel.setOnAction(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
if (onCancel.getCancellationAction() != null) {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
}
});
@@ -83,8 +83,8 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 0);
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 26);
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
private TaskExecutor executor;
private final JFXListView<Node> listView = new JFXListView<>();
@@ -106,18 +106,13 @@ public final class TaskListPane extends StackPane {
}
@FXThread
private void addStagesHints(@NotNull Collection<Task.StagesHint> hints) {
for (Task.StagesHint hint : hints) {
StageNode node = stageNodes.get(hint.stage());
if (node == null) {
node = new StageNode(hint.stage());
stageNodes.put(hint.stage(), node);
private void addStages(@NotNull Collection<String> stages) {
for (String stage : stages) {
stageNodes.computeIfAbsent(stage, s -> {
StageNode node = new StageNode(stage);
listView.getItems().add(node);
}
for (String stage : hint.aliases()) {
stageNodes.put(stage, node);
}
return node;
});
}
}
@@ -134,7 +129,7 @@ public final class TaskListPane extends StackPane {
Platform.runLater(() -> {
stageNodes.clear();
listView.getItems().clear();
addStagesHints(executor.getHints());
addStages(executor.getStages());
updateProgressNodePadding();
});
}
@@ -143,7 +138,7 @@ public final class TaskListPane extends StackPane {
public void onReady(Task<?> task) {
if (task instanceof Task.StagesHintTask) {
Platform.runLater(() -> {
addStagesHints(((Task<?>.StagesHintTask) task).getHints());
addStages(((Task<?>.StagesHintTask) task).getStages());
updateProgressNodePadding();
});
}
@@ -279,7 +274,6 @@ public final class TaskListPane extends StackPane {
private final class Cell extends ListCell<Node> {
private static final double STATUS_ICON_SIZE = 14;
private static final Insets PROGRESS_BAR_MARGIN = new Insets(2, 0, 0, 0);
private final BorderPane pane = new BorderPane();
private final StackPane left = new StackPane();
@@ -311,7 +305,6 @@ public final class TaskListPane extends StackPane {
bar.minWidthProperty().bind(barWidth);
bar.prefWidthProperty().bind(barWidth);
bar.maxWidthProperty().bind(barWidth);
BorderPane.setMargin(bar, PROGRESS_BAR_MARGIN);
setGraphic(pane);
}
@@ -327,9 +320,8 @@ public final class TaskListPane extends StackPane {
pane.paddingProperty().unbind();
title.textProperty().unbind();
message.textProperty().unbind();
bar.setSmoothProgress(false);
bar.progressProperty().unbind();
StageNode prevStageNode;
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
prevStageNode.status.removeListener(statusChangeListener);
@@ -367,8 +359,6 @@ public final class TaskListPane extends StackPane {
pane.setRight(null);
pane.setBottom(null);
}
bar.setSmoothProgress(true);
}
}
@@ -404,8 +394,6 @@ public final class TaskListPane extends StackPane {
}
private static final class StageNode extends Node {
private int runningTasksCount = 0;
private enum Status {
WAITING(SVG.MORE_HORIZ),
RUNNING(SVG.ARROW_FORWARD),
@@ -466,23 +454,17 @@ public final class TaskListPane extends StackPane {
}
private void begin() {
runningTasksCount++;
if (status.get() == Status.WAITING || status.get() == Status.SUCCESS) {
if (status.get() == Status.WAITING) {
status.set(Status.RUNNING);
}
}
public void succeed() {
runningTasksCount = Math.max(0, runningTasksCount - 1);
if (runningTasksCount == 0) {
status.set(Status.SUCCESS);
}
public void fail() {
status.set(Status.FAILED);
}
public void fail() {
runningTasksCount = Math.max(0, runningTasksCount - 1);
status.set(Status.FAILED);
public void succeed() {
status.set(Status.SUCCESS);
}
public void count() {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
@@ -169,13 +168,11 @@ public class TwoLineListItem extends VBox {
var scrollPane = new ScrollPane(tagsBox);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
scrollPane.setPrefWidth(50);
firstLine.getChildren().setAll(lblTitle, scrollPane);
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
}
return tags;
}
@@ -39,7 +39,6 @@ import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
@@ -47,10 +46,13 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class Decorator extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final StringProperty drawerTitle = new SimpleStringProperty();
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onRefreshNavButtonAction = new SimpleObjectProperty<>();
@@ -115,6 +117,18 @@ public class Decorator extends Control {
this.drawerWrapper = drawerWrapper;
}
public ObservableList<Node> getDrawer() {
return drawer.get();
}
public ListProperty<Node> drawerProperty() {
return drawer;
}
public void setDrawer(ObservableList<Node> drawer) {
this.drawer.set(drawer);
}
public ObservableList<Node> getContent() {
return content.get();
}
@@ -139,6 +153,30 @@ public class Decorator extends Control {
this.state.set(state);
}
public String getDrawerTitle() {
return drawerTitle.get();
}
public StringProperty drawerTitleProperty() {
return drawerTitle;
}
public void setDrawerTitle(String drawerTitle) {
this.drawerTitle.set(drawerTitle);
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public ObservableList<Node> getContainer() {
return container.get();
}
@@ -263,26 +301,7 @@ public class Decorator extends Control {
}
public void close() {
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(opacityProperty(), 1, Motion.EASE),
new KeyValue(scaleXProperty(), 1, Motion.EASE),
new KeyValue(scaleYProperty(), 1, Motion.EASE),
new KeyValue(scaleZProperty(), 0.3, Motion.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(opacityProperty(), 0, Motion.EASE),
new KeyValue(scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(scaleZProperty(), 0.8, Motion.EASE)
)
);
timeline.setOnFinished(event -> Launcher.stopApplication());
timeline.play();
} else {
Launcher.stopApplication();
}
onCloseButtonAction.get().run();
}
public void capableDraggingWindow(Node node) {
@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import com.jfoenix.controls.JFXSnackbarLayout;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Insets;
@@ -34,6 +37,7 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
@@ -43,6 +47,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogUtils;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
@@ -50,19 +55,20 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -75,6 +81,28 @@ public class DecoratorController {
public DecoratorController(Stage stage, Node mainPage) {
decorator = new Decorator(stage);
decorator.setOnCloseButtonAction(() -> {
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.opacityProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.3, Motion.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(decorator.opacityProperty(), 0, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.8, Motion.EASE)
)
);
timeline.setOnFinished(event -> Launcher.stopApplication());
timeline.play();
} else {
Launcher.stopApplication();
}
});
decorator.titleTransparentProperty().bind(config().titleTransparentProperty());
navigator = new Navigator();
@@ -193,10 +221,7 @@ public class DecoratorController {
String backgroundImage = config().getBackgroundImage();
if (backgroundImage != null)
try {
Path path = Path.of(backgroundImage);
image = Files.isDirectory(path)
? randomImageIn(path)
: tryLoadImage(path);
image = tryLoadImage(Paths.get(backgroundImage));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -218,7 +243,7 @@ public class DecoratorController {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
case PAINT:
Paint paint = config().getBackgroundPaint();
double opacity = MathUtils.clamp(config().getBackgroundImageOpacity(), 0, 100) / 100.;
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null)
@@ -302,14 +327,14 @@ public class DecoratorController {
return null;
}
ArrayList<Path> candidates;
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(it -> FXUtils.IMAGE_EXTENSIONS.contains(getExtension(it).toLowerCase(Locale.ROOT)))
.filter(Files::isReadable)
.collect(Collectors.toCollection(ArrayList::new));
.collect(toList());
} catch (IOException e) {
LOG.warning("Failed to list files in " + imageDir, e);
LOG.warning("Failed to list files in ./bg", e);
return null;
}
@@ -274,7 +274,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canBack) {
JFXButton backNavButton = new JFXButton();
skinnable.forbidDraggingWindow(backNavButton);
backNavButton.setFocusTraversable(false);
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
backNavButton.getStyleClass().add("jfx-decorator-button");
@@ -286,7 +285,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canClose) {
JFXButton closeNavButton = new JFXButton();
skinnable.forbidDraggingWindow(closeNavButton);
closeNavButton.setFocusTraversable(false);
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
closeNavButton.getStyleClass().add("jfx-decorator-button");
@@ -351,7 +349,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
refreshNavButton.getStyleClass().add("jfx-decorator-button");
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
skinnable.forbidDraggingWindow(refreshNavButton);
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
@@ -54,18 +54,13 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
@@ -89,8 +84,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, profile, __, mod, file) -> {
Versions.downloadModpackImpl(downloadProvider, profile, uploadVersion, mod, file);
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, mod, file) -> {
Versions.downloadModpackImpl(profile, uploadVersion, mod, file);
}, false);
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
@@ -99,9 +94,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
page.getActions().add(installLocalModpackButton);
return page;
}));
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks"), true)));
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks"), true)));
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, mod, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, mod, file) -> download(profile, version, file, "resourcepacks"), true)));
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((profile, version, mod, file) -> download(profile, version, file, "shaderpacks"), true)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
@@ -134,29 +129,16 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
};
}
public static void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Set<String> existingFiles;
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
existingFiles = list.map(Path::getFileName)
.map(Path::toString)
.collect(Collectors.toSet());
} catch (IOException e) {
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
existingFiles = Set.of();
}
Set<String> finalExistingFiles = existingFiles;
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> {
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.getFile().getUrl()), dest);
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
@@ -171,7 +153,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
handler.resolve();
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
}
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.i18n.I18n;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -51,11 +50,11 @@ public class InstallersPage extends AbstractInstallersPage {
@Override
public String getTitle() {
return ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
return group.getGame().versionProperty().get().version();
}
private String getVersion(String id) {
return I18n.getDisplayVersion((RemoteVersion) controller.getSettings().get(id));
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
}
protected void reload() {
@@ -112,7 +111,7 @@ public class InstallersPage extends AbstractInstallersPage {
}
private void setTxtNameWithLoaders() {
StringBuilder nameBuilder = new StringBuilder(getTitle());
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().version());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
@@ -121,22 +120,23 @@ public class InstallersPage extends AbstractInstallersPage {
}
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName = switch (libraryType) {
case FORGE -> "Forge";
case NEO_FORGE -> "NeoForge";
case CLEANROOM -> "Cleanroom";
case LEGACY_FABRIC -> "LegacyFabric";
case FABRIC -> "Fabric";
case LITELOADER -> "LiteLoader";
case QUILT -> "Quilt";
case OPTIFINE -> "OptiFine";
case FORGE -> i18n("install.installer.forge");
case NEO_FORGE -> i18n("install.installer.neoforge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric").replace(" ", "_");
case FABRIC -> i18n("install.installer.fabric");
case LITELOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
case OPTIFINE -> i18n("install.installer.optifine");
default -> null;
};
if (loaderName != null)
nameBuilder.append('-').append(loaderName);
if (loaderName == null)
continue;
nameBuilder.append("-").append(loaderName);
}
}
@@ -47,7 +47,6 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
private final Path file;
private final String updateVersion;
private String iconUrl;
private boolean hasSource;
public ModpackInstallWizardProvider(Profile profile) {
this(profile, null, null);
@@ -80,7 +79,6 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
if (StringUtils.isNotBlank(iconUrl))
settings.put(LocalModpackPage.MODPACK_ICON_URL, iconUrl);
settings.put(ModpackPage.PROFILE, profile);
hasSource = settings.containsKey(LocalModpackPage.MODPACK_FILE) || settings.containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
}
private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
@@ -147,30 +145,20 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
return finishModpackInstallingAsync(settings);
}
private static Node createModpackInstallPage(WizardController controller) {
if (controller.getSettings().containsKey(LocalModpackPage.MODPACK_FILE))
return new LocalModpackPage(controller);
else if (controller.getSettings().containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST))
return new RemoteModpackPage(controller);
else
throw new IllegalArgumentException();
}
@Override
public Node createPage(WizardController controller, int step, SettingsMap settings) {
if (hasSource) {
return switch (step) {
case 0 -> createModpackInstallPage(controller);
default -> throw new IllegalStateException(
"error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
};
} else {
return switch (step) {
case 0 -> new ModpackSelectionPage(controller);
case 1 -> createModpackInstallPage(controller);
default -> throw new IllegalStateException(
"error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
};
switch (step) {
case 0:
return new ModpackSelectionPage(controller);
case 1:
if (controller.getSettings().containsKey(LocalModpackPage.MODPACK_FILE))
return new LocalModpackPage(controller);
else if (controller.getSettings().containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST))
return new RemoteModpackPage(controller);
else
throw new IllegalArgumentException();
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@@ -1,20 +1,3 @@
/*
* 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.download;
import com.jfoenix.controls.JFXButton;
@@ -59,11 +42,11 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
{
var archiveNamePane = new LinePane();
{
archiveNamePane.setTitle(i18n("version.name"));
archiveNamePane.setTitle(i18n("archive.file.name"));
txtModpackName = new JFXTextField();
txtModpackName.setPrefWidth(300);
FXUtils.setLimitHeight(archiveNamePane, 75);
// FIXME: Validator are not shown properly
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setRight(txtModpackName);
@@ -97,7 +97,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
graphic.setMouseTransparent(true);
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
SVGPath arrow = SVG.ARROW_FORWARD.createIcon();
SVGPath arrow = new SVGPath();
arrow.setContent(SVG.ARROW_FORWARD.getPath());
BorderPane.setAlignment(arrow, Pos.CENTER);
graphic.setRight(arrow);
@@ -143,6 +144,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
// otherwise we still consider the file as modpack zip file
// since casually the url may not ends with ".zip"
Path modpack = Files.createTempFile("modpack", ".zip");
handler.resolve();
Controllers.taskDialog(
new FileDownloadTask(url, modpack)
.whenComplete(Schedulers.javafx(), e -> {
@@ -40,6 +40,7 @@ import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.zip.ZipException;
@@ -77,21 +78,21 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
// We remove library but not save it,
// so if installation failed will not break down current version.
Task<Version> ret = Task.supplyAsync(() -> version);
var hints = new ArrayList<Task.StagesHint>();
List<String> stages = new ArrayList<>();
for (Object value : settings.asStringMap().values()) {
if (value instanceof RemoteVersion remoteVersion) {
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
hints.add(new Task.StagesHint(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion())));
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
if ("game".equals(remoteVersion.getLibraryId())) {
hints.add(new Task.StagesHint("hmcl.install.libraries"));
hints.add(new Task.StagesHint("hmcl.install.assets"));
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
}
} else if (value instanceof RemoveVersionAction removeVersionAction) {
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, removeVersionAction.libraryId));
}
}
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHints(hints);
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
}
@Override
@@ -278,12 +278,10 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
else
twoLineListItem.addTag(displayGameVersion);
twoLineListItem.addTag(remoteVersion.getGameVersion());
}
}
}
@@ -221,15 +221,10 @@ public final class ModpackInfoPage extends Control implements WizardPage {
list.getContent().addAll(
createTextFieldLinePane(i18n("modpack.name"), skinnable.name, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.version"), skinnable.version, new RequiredValidator())
);
if (skinnable.options.isRequireAuthor()) {
list.getContent().add(
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator())
);
}
if (skinnable.options.isRequireFileApi()) {
list.getContent().add(createTextFieldLinePane(
i18n("modpack.file_api"), skinnable.fileApi,
@@ -19,17 +19,9 @@ package org.jackhuang.hmcl.ui.image;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import org.girod.javafx.svgimage.LoaderParameters;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
@@ -47,9 +39,7 @@ import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -83,46 +73,6 @@ public final class ImageUtils {
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
@@ -186,13 +136,11 @@ public final class ImageUtils {
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"svg", SVG,
"apng", APNG
);
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/svg+xml", SVG,
"image/apng", APNG
);
@@ -217,14 +165,6 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}
private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
@@ -292,8 +232,6 @@ public final class ImageUtils {
return WEBP;
if (isApng(headerBuffer))
return APNG;
if (isSVG(headerBuffer))
return SVG;
return null;
}
@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.*;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
@@ -28,7 +30,6 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
@@ -37,18 +38,11 @@ import java.io.InputStream;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends SpinnerPane {
public final class AboutPage extends StackPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList about = new ComponentList();
{
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
@@ -90,16 +84,27 @@ public final class AboutPage extends SpinnerPane {
legal.getContent().setAll(copyright, claim, openSource);
}
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static Image loadImage(String url) {
@@ -85,13 +85,7 @@ public class DownloadSettingsPage extends StackPane {
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
var defaultAddonSourcePane = new LineSelectButton<String>();
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setItems("modrinth", "curseforge");
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes;
@@ -36,7 +37,9 @@ public class FeedbackPage extends SpinnerPane {
public FeedbackPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -81,5 +84,8 @@ public class FeedbackPage extends SpinnerPane {
ComponentList.createComponentListTitle(i18n("contact.feedback")),
feedback
);
this.setContent(content);
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
@@ -41,7 +42,9 @@ public class HelpPage extends SpinnerPane {
public HelpPage() {
content = new VBox();
content.getStyleClass().add("spinner-pane-content");
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -47,7 +47,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
@@ -234,6 +233,7 @@ public final class JavaDownloadDialog extends StackPane {
downloadButton.setOnAction(e -> onDownload());
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
downloadButtonPane.getChildren().setAll(downloadButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
@@ -300,11 +300,6 @@ public final class JavaDownloadDialog extends StackPane {
remoteVersionBox.getSelectionModel().selectFirst();
});
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("small-spinner-pane");
spinnerPane.setContent(downloadButton);
downloadButtonPane.getChildren().setAll(spinnerPane);
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
remoteVersionBox.setItems(null);
packageTypeBox.getItems().clear();
@@ -313,11 +308,11 @@ public final class JavaDownloadDialog extends StackPane {
warningLabel.setText(null);
if (list == null || (list.versions != null && list.versions.isEmpty()))
spinnerPane.hideSpinner();
downloadButtonPane.getChildren().setAll(downloadButton);
else if (list.status == DiscoJavaVersionList.Status.LOADING)
spinnerPane.showSpinner();
downloadButtonPane.getChildren().setAll(new JFXSpinner());
else {
spinnerPane.hideSpinner();
downloadButtonPane.getChildren().setAll(downloadButton);
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
packageTypeBox.getItems().setAll(list.versions.keySet());
@@ -241,7 +241,9 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
JFXButton revealButton = new JFXButton();
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setOnAction(e -> {
JavaRuntime java = getItem();
if (java != null)
@@ -157,20 +157,26 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
if (skinnable.realPath == null) {
revealButton.setDisable(true);
JFXButton removeButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(SVG.DELETE.createIcon());
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
right.getChildren().setAll(revealButton, removeButton);
} else {
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(SVG.RESTORE.createIcon());
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
@@ -227,7 +227,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchLabel.setText(i18n("version.launch.empty"));
currentLabel.setText(null);
graphic.getChildren().setAll(launchLabel);
FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launchNoGame);
launchButton.setOnAction(e -> MainPage.this.launchNoGame());
if (tooltip == null)
tooltip = new Tooltip(i18n("version.launch.empty.tooltip"));
FXUtils.installFastTooltip(launchButton, tooltip);
@@ -235,7 +235,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchLabel.setText(i18n("version.launch"));
currentLabel.setText(currentGame);
graphic.getChildren().setAll(launchLabel, currentLabel);
FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launch);
launchButton.setOnAction(e -> MainPage.this.launch());
if (tooltip != null)
Tooltip.uninstall(launchButton, tooltip);
}
@@ -277,9 +277,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void showUpdate(boolean show) {
doAnimation(show);
if (show && !config().isDisableAutoShowUpdateDialog()
&& getLatestVersion() != null
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
.addAction(i18n("button.view"), () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
@@ -226,7 +226,9 @@ public class PersonalizationPage extends StackPane {
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtLogFontSize)));
JFXButton clearButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton clearButton = new JFXButton();
clearButton.getStyleClass().add("toggle-icon4");
clearButton.setGraphic(SVG.RESTORE.createIcon());
clearButton.setOnAction(e -> cboLogFont.setValue(null));
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
@@ -274,7 +276,9 @@ public class PersonalizationPage extends StackPane {
cboFont.setValue(config().getLauncherFontFamily());
FXUtils.onChange(cboFont.valueProperty(), FontManager::setFontFamily);
JFXButton clearButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton clearButton = new JFXButton();
clearButton.getStyleClass().add("toggle-icon4");
clearButton.setGraphic(SVG.RESTORE.createIcon());
clearButton.setOnAction(e -> cboFont.setValue(null));
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
@@ -54,7 +54,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -99,7 +98,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage,
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file) || "json".equalsIgnoreCase(FileUtils.getExtension(file)),
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
modpacks -> {
Path file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
@@ -114,8 +113,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
});
@@ -18,24 +18,24 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -45,8 +45,6 @@ import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.AprilFools;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
@@ -62,16 +60,20 @@ import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SettingsPage extends ScrollPane {
@SuppressWarnings("FieldCanBeLocal")
private final ToggleGroup updateChannelGroup;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateListener;
@@ -118,59 +120,84 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(sponsorPane);
}
ObjectProperty<UpdateChannel> updateChannel;
{
JFXButton updateButton = new JFXButton();
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
var updatePane = new LineSelectButton<UpdateChannel>() {
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
updateChannel = updatePane.valueProperty();
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setValue(UpdateChannel.getChannel());
updatePane.setHasSubtitle(true);
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
final Label lblUpdate;
final Label lblUpdateSub;
{
VBox headerLeft = new VBox();
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
{
updateListener = any -> {
boolean outdated = UpdateChecker.isOutdated();
JFXButton btnUpdate = new JFXButton();
btnUpdate.setOnAction(e -> onUpdate());
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.UPDATE.createIcon(20));
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
if (UpdateChecker.isOutdated()) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSubProperty.set(i18n("update.checking"));
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
} else {
lblUpdateSubProperty.set(i18n("update.latest"));
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox(12);
content.setPadding(new Insets(8, 0, 0, 0));
updateChannelGroup = new ToggleGroup();
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateStable.setUserData(UpdateChannel.STABLE);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateDev.setToggleGroup(updateChannelGroup);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
@@ -180,6 +207,8 @@ public final class SettingsPage extends ScrollPane {
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
@@ -189,22 +218,6 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(previewPane);
}
{
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
}
if (AprilFools.isShowAprilFoolsSettings()) {
LineToggleButton disableAprilFools = new LineToggleButton();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
settingsPane.getContent().add(disableAprilFools);
}
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
@@ -260,15 +273,6 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
LineToggleButton allowAutoAgentPane = new LineToggleButton();
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
settingsPane.getContent().add(allowAutoAgentPane);
}
{
BorderPane debugPane = new BorderPane();
@@ -282,31 +286,12 @@ public final class SettingsPage extends ScrollPane {
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
logButton.setOnAction(e -> onExportLogs());
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
@@ -378,79 +363,86 @@ public final class SettingsPage extends ScrollPane {
}
}
private CompletableFuture<Path> onExportLogs() {
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
private void onExportLogs() {
thread(() -> {
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
Path outputFile;
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
try {
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
LOG.info("Exporting latest logs to " + outputFile);
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
Set<String> entryNames = new HashSet<>();
Set<String> entryNames = new HashSet<>();
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
}
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
continue;
}
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
}
// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
} catch (IOException e) {
LOG.warning("Failed to export logs", e);
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
return;
}
return outputFile;
}), Schedulers.io());
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
FXUtils.showFileInExplorer(outputFile);
});
}
private void onSponsor() {
@@ -22,14 +22,8 @@ import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.tag.Tag;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -39,7 +33,6 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Path;
@@ -63,9 +56,8 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString())));
this.file = file;
//noinspection DataFlowIssue
this.type = NBTFileType.ofFile(file);
if (type == null) {
throw new IOException("Unknown type of file " + file);
}
@@ -93,33 +85,14 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
actions.getChildren().setAll(saveButton, cancelButton);
Task.supplyAsync(() -> type.read(file))
Task.supplyAsync(() -> type.readAsTree(file))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
setLoading(false);
NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file));
var view = new TreeView<>(root) {
@Override
protected Skin<?> createDefaultSkin() {
return new TreeViewSkin<>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
};
view.setCellFactory(ignored -> new NBTTreeCell());
view.addEventHandler(TreeItem.<NBTElement>branchExpandedEvent(), event -> {
TreeItem<NBTElement> item = event.getTreeItem();
if (item.getValue() instanceof Tag && item.getChildren().size() == 1)
item.getChildren().get(0).setExpanded(true);
});
root.setExpanded(true);
NBTTreeView view = new NBTTreeView(result);
BorderPane.setMargin(view, new Insets(10));
onEscPressed(view, cancelButton::fire);
this.root.setCenter(view);
root.setCenter(view);
} else {
LOG.warning("Fail to open nbt file", exception);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
@@ -17,34 +17,148 @@
*/
package org.jackhuang.hmcl.ui.nbt;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.io.NBTCodec;
import org.glavo.nbt.tag.Tag;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import kala.compress.utils.BoundedInputStream;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/// @author Glavo
/**
* @author Glavo
*/
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
@Override
public Tag read(Path file) throws IOException {
return NBTCodec.of().readTag(file);
try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) {
fileInputStream.mark(3);
byte[] header = new byte[3];
if (fileInputStream.read(header) < 3) {
throw new IOException("File is too small");
}
fileInputStream.reset();
InputStream input;
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
input = new GZIPInputStream(fileInputStream);
} else {
input = fileInputStream;
}
Tag tag = NBTIO.readTag(input);
if (!(tag instanceof CompoundTag))
throw new IOException("Unexpected tag: " + tag);
return tag;
}
}
},
ANVIL("mca") {
@Override
public NBTElement read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
public Tag read(Path file) throws IOException {
return REGION.read(file);
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
return REGION.readAsTree(file);
}
},
REGION("mcr") {
@Override
public ChunkRegion read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
public Tag read(Path file) throws IOException {
try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) {
ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class);
if (r.length() == 0) {
return tag;
}
byte[] header = new byte[4096];
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
Inflater inflater = new Inflater();
r.readFully(header);
for (int i = 0; i < 4096; i += 4) {
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
int length = header[i + 3] & 0xff;
if (offset == 0 || length == 0) {
continue;
}
r.seek(offset * 4096L);
r.readFully(buffer, 0, length * 4096);
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
InputStream input = new ByteArrayInputStream(buffer);
input.skip(5);
input = new BoundedInputStream(input, chunkLength - 1);
switch (buffer[4]) {
case 0x01:
// GZip
input = new GZIPInputStream(input);
break;
case 0x02:
// Zlib
inflater.reset();
input = new InflaterInputStream(input, inflater);
break;
case 0x03:
// Uncompressed
break;
default:
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
}
try (InputStream in = input) {
Tag chunk = NBTIO.readTag(in);
if (!(chunk instanceof CompoundTag))
throw new IOException("Unexpected tag: " + chunk);
tag.add(chunk);
}
}
return tag;
}
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
for (Tag tag : ((ListTag) item.getValue())) {
CompoundTag chunk = (CompoundTag) tag;
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
Tag xPos = chunk.get("xPos");
Tag zPos = chunk.get("zPos");
if (xPos instanceof IntTag && zPos instanceof IntTag) {
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
} else {
tree.setText("Chunk: Unknown");
}
item.getChildren().add(tree);
}
return item;
}
};
@@ -54,7 +168,7 @@ public enum NBTFileType {
return NBTFileType.ofFile(file) != null;
}
public static @Nullable NBTFileType ofFile(Path file) {
public static NBTFileType ofFile(Path file) {
String ext = FileUtils.getExtension(file);
for (NBTFileType type : types) {
for (String extension : type.extensions) {
@@ -72,6 +186,11 @@ public enum NBTFileType {
this.extensions = extensions;
}
public abstract NBTElement read(Path file) throws IOException;
public abstract Tag read(Path file) throws IOException;
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
root.setName(file.getFileName().toString());
return root;
}
}
@@ -0,0 +1,79 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.nbt;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* @author Glavo
*/
public enum NBTTagType {
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
STRING,
LIST, COMPOUND;
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
static {
for (NBTTagType type : values()) {
lookupTable.put(type.getTagClassName(), type);
}
}
public static NBTTagType typeOf(Tag tag) {
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
if (type == null) {
throw new IllegalArgumentException("Unknown tag: " + type);
}
return type;
}
private final String iconUrl;
private final String tagClassName;
NBTTagType() {
String tagName;
String className;
int idx = name().indexOf('_');
if (idx < 0) {
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
className = tagName + "Tag";
} else {
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
}
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
this.tagClassName = className;
}
public String getIconUrl() {
return iconUrl;
}
public String getTagClassName() {
return tagClassName;
}
}
@@ -1,129 +0,0 @@
/*
* 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.nbt;
import javafx.scene.control.TreeCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> {
private static @Nullable Image getIcon(NBTElement element) {
if (element instanceof Tag tag) {
TagType<?> type = tag.getType();
if (type == TagType.BYTE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png");
} else if (type == TagType.SHORT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png");
} else if (type == TagType.INT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png");
} else if (type == TagType.LONG) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png");
} else if (type == TagType.FLOAT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png");
} else if (type == TagType.DOUBLE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png");
} else if (type == TagType.STRING) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png");
} else if (type == TagType.BYTE_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png");
} else if (type == TagType.INT_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png");
} else if (type == TagType.LONG_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png");
} else if (type == TagType.LIST) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
} else if (type == TagType.COMPOUND) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
} else {
return null;
}
} else if (element instanceof ChunkRegion)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
else if (element instanceof Chunk)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
else
return null;
}
private final ImageView imageView;
public NBTTreeCell() {
imageView = new ImageView();
imageView.setFitHeight(16);
imageView.setFitWidth(16);
setGraphic(imageView);
}
private NBTTreeItem getNBTTreeItem() {
return (NBTTreeItem) getTreeItem();
}
@Override
public void updateItem(@Nullable NBTElement item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
imageView.setImage(null);
setText(null);
return;
}
imageView.setImage(getIcon(item));
String name = getNBTTreeItem().getOverrideName();
if (name == null) {
NBTElement value = getNBTTreeItem().getValue();
if (value instanceof Tag tag) {
if (tag.getParent() instanceof ListTag<?>) {
name = Integer.toString(tag.getIndex());
} else {
name = tag.getName();
}
} else if (value instanceof Chunk chunk) {
name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")";
} else {
name = "";
}
}
String text;
if (item instanceof ParentTag<?> parentTag) {
text = i18n("nbt.entries", parentTag.size());
} else if (item instanceof ValueTag<?> valueTag) {
text = valueTag.getAsString();
} else {
text = null;
}
if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
}
@@ -1,68 +0,0 @@
/*
* 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.nbt;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.NBTParent;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.tag.*;
import org.jetbrains.annotations.Nullable;
/// @author Glavo
public final class NBTTreeItem extends TreeItem<NBTElement> {
private final @Nullable String overrideName;
public NBTTreeItem(NBTElement value) {
this(value, null);
}
public NBTTreeItem(NBTElement value, @Nullable String overrideName) {
super(value);
this.overrideName = overrideName;
}
@Override
public boolean isLeaf() {
return !(getValue() instanceof NBTParent<?> parent) || parent.isEmpty();
}
private boolean needInitChildren = true;
@Override
public ObservableList<TreeItem<NBTElement>> getChildren() {
ObservableList<TreeItem<NBTElement>> children = super.getChildren();
if (needInitChildren) {
needInitChildren = false;
if (getValue() instanceof Chunk chunk) {
if (chunk.getRootTag() != null) {
children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList());
}
} else if (getValue() instanceof NBTParent<?> parent) {
children.setAll(parent.stream().map(NBTTreeItem::new).toList());
}
}
return children;
}
public @Nullable String getOverrideName() {
return overrideName;
}
}
@@ -0,0 +1,184 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.nbt;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.jackhuang.hmcl.ui.FXUtils;
import java.lang.reflect.Array;
import java.util.EnumMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
if (tree != null) tree.setExpanded(true);
this.setCellFactory(cellFactory());
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new TreeViewSkin<Tag>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
return view -> new TreeCell<>() {
private void setTagText(String text) {
String name = ((Item) getTreeItem()).getName();
if (name == null) {
setText(text);
} else if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
private void setTagText(int nEntries) {
setTagText(i18n("nbt.entries", nEntries));
}
@Override
public void updateItem(Tag item, boolean empty) {
super.updateItem(item, empty);
ImageView imageView = (ImageView) this.getGraphic();
if (imageView == null) {
imageView = new ImageView();
this.setGraphic(imageView);
}
if (item == null) {
imageView.setImage(null);
setText(null);
return;
}
NBTTagType tagType = NBTTagType.typeOf(item);
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
imageView.setFitHeight(16);
imageView.setFitWidth(16);
if (((Item) getTreeItem()).getText() != null) {
setText(((Item) getTreeItem()).getText());
} else {
switch (tagType) {
case BYTE:
case SHORT:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case STRING:
setTagText(item.getValue().toString());
break;
case BYTE_ARRAY:
case INT_ARRAY:
case LONG_ARRAY:
setTagText(Array.getLength(item.getValue()));
break;
case LIST:
setTagText(((ListTag) item).size());
break;
case COMPOUND:
setTagText(((CompoundTag) item).size());
break;
default:
setTagText(null);
}
}
}
};
}
public static Item buildTree(Tag tag) {
Item item = new Item(tag);
if (tag instanceof CompoundTag) {
for (Tag subTag : ((CompoundTag) tag)) {
item.getChildren().add(buildTree(subTag));
}
} else if (tag instanceof ListTag) {
int idx = 0;
for (Tag subTag : ((ListTag) tag)) {
Item subTree = buildTree(subTag);
subTree.setName(String.valueOf(idx++));
item.getChildren().add(subTree);
}
}
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
});
return item;
}
public CompoundTag getRootTag() {
return ((CompoundTag) getRoot().getValue());
}
public static class Item extends TreeItem<Tag> {
private String text;
private String name;
public Item() {
}
public Item(Tag value) {
super(value);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name == null ? getValue().getName() : name;
}
}
}
@@ -60,9 +60,11 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.CLOSE, 14);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -33,7 +33,7 @@ public class SkinCube extends MeshView {
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
float startX, float startY, boolean isSlim) {
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = half_width,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
return new float[]{
top_x, top_y, // T0 ---
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff. Show more