Compare commits

..
190 changed files with 3270 additions and 10758 deletions

No files matched your search

-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/
+2 -2
View File
@@ -28,6 +28,7 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
@@ -59,7 +60,6 @@ dependencies {
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
@@ -129,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",
@@ -153,6 +152,7 @@ val hmclProperties = buildList {
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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)
};
/**
@@ -34,17 +34,14 @@ import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* @author Shadi Shaheen
@@ -98,17 +95,14 @@ public class JFXCustomColorPickerDialog extends StackPane {
rgbField.getStyleClass().add("custom-color-field");
rgbField.setPromptText("RGB Color");
rgbField.setTextFormatter(colorCharFormatter());
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hsbField.getStyleClass().add("custom-color-field");
hsbField.setPromptText("HSB Color");
hsbField.setTextFormatter(colorCharFormatter());
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hexField.getStyleClass().add("custom-color-field");
hexField.setPromptText("#HEX Color");
hexField.setTextFormatter(colorCharFormatter());
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
StackPane tabContent = new StackPane();
@@ -309,9 +303,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() {
@@ -413,20 +404,4 @@ public class JFXCustomColorPickerDialog extends StackPane {
dialog.setMinWidth(minWidth);
dialog.setMinHeight(minHeight);
}
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
private static TextFormatter<String> colorCharFormatter() {
return new TextFormatter<>(change -> {
if (!change.isContentChange()) return change;
String ins = StringUtils.toHalfWidth(change.getText());
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
String full = StringUtils.toHalfWidth(change.getControlNewText());
long h = full.chars().filter(c -> c == '#').count();
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
change.setText(ins);
return change;
});
}
}
@@ -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();
}
}
}
@@ -200,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);
@@ -249,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() {
@@ -387,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(
@@ -411,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)
@@ -451,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 {
@@ -486,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);
@@ -583,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);
}
}
@@ -167,7 +167,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
}
public static boolean isExternalGameNameConflicts(String name) {
@@ -211,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("hmcl.modpack", "hmcl.modpack.download");
.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("hmcl.modpack", "hmcl.modpack.download");
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.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("hmcl.modpack", "hmcl.modpack.download");
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
@@ -242,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) {
@@ -22,28 +22,27 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.*;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
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 OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
private final String codeVerifier;
private final String state;
public static String lastlyOpenedURL;
@@ -53,34 +52,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
super(port);
this.port = port;
var encoder = Base64.getUrlEncoder().withoutPadding();
var random = new SecureRandom();
{
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
byte[] bytes = new byte[32];
random.nextBytes(bytes);
this.state = encoder.encodeToString(bytes);
}
{
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
byte[] bytes = new byte[64];
random.nextBytes(bytes);
this.codeVerifier = encoder.encodeToString(bytes);
}
}
@Override
public String getCodeVerifier() {
return codeVerifier;
}
@Override
public String getState() {
return state;
}
@Override
@@ -122,32 +93,19 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
String code = query.get("code");
if (code != null) {
if (this.state.equals(query.get("state"))) {
idToken = query.get("id_token");
future.complete(code);
} else if (query.containsKey("state")) {
LOG.warning("Failed to authenticate: invalid state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
} else {
LOG.warning("Failed to authenticate: missing state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
}
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
LOG.warning("Failed to authenticate: missing authorization code in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -165,8 +123,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
@@ -193,13 +150,17 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
@Override
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
switch (grantFlow) {
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
@@ -207,6 +168,17 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
@@ -304,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 {
@@ -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();
@@ -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
@@ -231,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)) {
@@ -440,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) {
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -49,10 +49,10 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown, false);
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
@@ -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;
@@ -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);
@@ -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));
}
}
@@ -342,12 +343,13 @@ public class InstallerItem extends Control {
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
HBox buttonsContainer = new HBox();
buttonsContainer.setPickOnBounds(false);
buttonsContainer.setSpacing(8);
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;
@@ -40,12 +43,8 @@ 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;
@@ -258,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);
@@ -267,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();
@@ -383,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());
}
});
@@ -411,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);
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.value.ObservableValue;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
@@ -28,7 +30,6 @@ import javafx.scene.shape.SVGPath;
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
/// The view boxes of all icons are normalized to `0 0 24 24`.
public enum SVG {
NONE(""), // Empty Icon
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
@@ -129,12 +130,6 @@ public enum SVG {
public static final double DEFAULT_SIZE = 24;
static void setSize(SVGPath path, double size) {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
}
private final String rawPath;
private String path;
@@ -149,20 +144,35 @@ public enum SVG {
return path;
}
public SVGPath createIcon() {
var path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(getPath());
return path;
public SVGPath createSVGPath() {
var p = new SVGPath();
p.setContent(getPath());
p.getStyleClass().add("svg");
return p;
}
public SVGContainer createIcon(double size) {
return new SVGContainer(this, size);
private static Node createIcon(SVGPath path, double size) {
if (size == DEFAULT_SIZE)
return path;
else {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
}
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
SVGPath path = createIcon();
path.fillProperty().bind(color);
return path;
public Node createIcon() {
return createIcon(DEFAULT_SIZE);
}
public Node createIcon(double size) {
return createIcon(createSVGPath(), size);
}
public Node createIcon(ObservableValue<? extends Paint> color) {
SVGPath p = createSVGPath();
p.fillProperty().bind(color);
return createIcon(p, DEFAULT_SIZE);
}
}
@@ -1,175 +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.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Parent;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
/// A lightweight wrapper for displaying [SVG] icons.
///
/// @author Glavo
public final class SVGContainer extends Parent {
private static final String DEFAULT_STYLE_CLASS = "svg-container";
private final SVGPath path = new SVGPath();
private SVG icon = SVG.NONE;
private double iconSize = SVG.DEFAULT_SIZE;
private SVGPath tempPath;
private Timeline timeline;
{
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.path.getStyleClass().add("svg");
}
/// Creates an SVGContainer with the default icon and the default icon size.
public SVGContainer() {
this(SVG.NONE, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer showing the given icon using the default icon size.
///
/// @param icon the [SVG] icon to display
public SVGContainer(SVG icon) {
this(icon, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer with a custom icon size. The initial icon is
/// [SVG#NONE].
///
/// @param iconSize the icon size
public SVGContainer(double iconSize) {
this(SVG.NONE, iconSize);
}
/// Creates an SVGContainer with the specified icon and size.
///
/// @param icon the [SVG] icon to display
/// @param iconSize the icon size
public SVGContainer(SVG icon, double iconSize) {
setIconSizeImpl(iconSize);
setIcon(icon);
}
public double getIconSize() {
return iconSize;
}
private void setIconSizeImpl(double newSize) {
this.iconSize = newSize;
SVG.setSize(path, newSize);
if (tempPath != null)
SVG.setSize(tempPath, newSize);
}
public void setIconSize(double newSize) {
setIconSizeImpl(newSize);
requestLayout();
}
/// Gets the currently displayed icon.
public SVG getIcon() {
return icon;
}
/// Sets the icon to display without animation.
public void setIcon(SVG newIcon) {
setIcon(newIcon, Duration.ZERO);
}
/// Sets the icon to display with a cross-fade animation.
public void setIcon(SVG newIcon, Duration animationDuration) {
if (timeline != null) {
timeline.stop();
timeline = null;
}
SVG oldIcon = this.icon;
this.icon = newIcon;
if (animationDuration.equals(Duration.ZERO)) {
path.setContent(newIcon.getPath());
path.setOpacity(1);
if (getChildren().size() != 1)
getChildren().setAll(path);
} else {
if (tempPath == null) {
tempPath = new SVGPath();
tempPath.getStyleClass().add("svg");
SVG.setSize(tempPath, iconSize);
} else
tempPath.setOpacity(1);
tempPath.setContent(oldIcon.getPath());
getChildren().setAll(path, tempPath);
path.setOpacity(0);
path.setContent(newIcon.getPath());
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
),
new KeyFrame(animationDuration,
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
)
);
timeline.setOnFinished(e -> {
getChildren().setAll(path);
timeline = null;
});
timeline.play();
}
}
// Parent
@Override
public double prefWidth(double height) {
return iconSize;
}
@Override
public double prefHeight(double width) {
return iconSize;
}
@Override
public double minHeight(double width) {
return iconSize;
}
@Override
public double minWidth(double height) {
return iconSize;
}
@Override
protected void layoutChildren() {
// Do nothing
}
}
@@ -20,19 +20,14 @@ package org.jackhuang.hmcl.ui;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.EventPriority;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public final class WeakListenerHolder {
public class WeakListenerHolder {
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
@@ -53,22 +48,6 @@ public final class WeakListenerHolder {
return new WeakListChangeListener<>(listener);
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer) {
refs.add(manager.registerWeak(consumer));
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer, EventPriority priority) {
refs.add(manager.registerWeak(consumer, priority));
}
public <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChange(value, consumer));
}
public <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChangeAndOperate(value, consumer));
}
public void add(Object obj) {
refs.add(obj);
}
@@ -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);
@@ -130,7 +130,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftIcon(SVG.MICROSOFT);
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
@@ -23,7 +23,9 @@ import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -43,6 +45,7 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
@@ -51,6 +54,7 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.StringUtils;
@@ -81,11 +85,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final JFXButton btnAccept;
private final SpinnerPane spinner;
private final Node body;
private final HBox actions;
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@@ -140,10 +146,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnCancel.setOnAction(e -> onCancel());
onEscPressed(this, btnCancel::fire);
actions = new HBox(spinner, btnCancel);
actions.setAlignment(Pos.CENTER_RIGHT);
HBox hbox = new HBox(spinner, btnCancel);
hbox.setAlignment(Pos.CENTER_RIGHT);
setActions(lblErrorMessage, actions);
setActions(lblErrorMessage, hbox);
}
if (showMethodSwitcher) {
@@ -220,6 +226,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
Runnable doCreate = () -> {
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@@ -273,16 +280,93 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnAccept.disableProperty().unbind();
detailsContainer.getChildren().remove(detailsPane);
lblErrorMessage.setText("");
setActions(lblErrorMessage, actions);
lblErrorMessage.setVisible(true);
}
if (factory == Accounts.FACTORY_MICROSOFT) {
detailsPane = new MicrosoftAccountLoginPane(true);
setActions();
VBox vbox = new VBox(8);
detailsPane = vbox;
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().add(hintPane);
return;
}
if (!IntegrityChecker.isOfficial()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("unofficial.hint"));
vbox.getChildren().add(hintPane);
}
VBox codeBox = new VBox(8);
Label hint = new Label(i18n("account.methods.microsoft.code"));
Label code = new Label();
code.setMouseTransparent(true);
code.setStyle("-fx-font-size: 24");
codeBox.getChildren().addAll(hint, code);
codeBox.setAlignment(Pos.CENTER);
vbox.getChildren().add(codeBox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setVisible(false);
errHintPane.setManaged(false);
codeBox.setVisible(false);
codeBox.setManaged(false);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
code.setText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
codeBox.setVisible(true);
codeBox.setManaged(true);
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
codeBox.setVisible(false);
codeBox.setManaged(false);
}
});
lblErrorMessage.setVisible(false);
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
boolean hasError = !newVal.isEmpty();
errHintPane.setSegment(newVal);
errHintPane.setVisible(hasError);
errHintPane.setManaged(hasError);
hintPane.setVisible(!hasError);
hintPane.setManaged(!hasError);
codeBox.setVisible(!hasError && deviceCode.get() != null);
codeBox.setManaged(!hasError && deviceCode.get() != null);
});
FXUtils.onClicked(codeBox, () -> {
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
runInFX(() -> deviceCode.set(value))
));
HBox linkBox = new HBox();
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
btnAccept.setDisable(false);
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
setActions(lblErrorMessage, actions);
}
detailsContainer.getChildren().add(detailsPane);
@@ -399,7 +483,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 +650,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 +662,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,320 +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.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import io.nayuki.qrcodegen.QrCode;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.SVGPath;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.QrCodeUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware {
private final Account accountToRelogin;
private final Consumer<AuthInfo> loginCallback;
private final Runnable cancelCallback;
@SuppressWarnings("FieldCanBeLocal")
private final WeakListenerHolder holder = new WeakListenerHolder();
private final ObjectProperty<Step> step = new SimpleObjectProperty<>();
private TaskExecutor browserTaskExecutor;
private TaskExecutor deviceTaskExecutor;
private final JFXButton btnLogin;
private final SpinnerPane loginButtonSpinner;
public MicrosoftAccountLoginPane() {
this(false);
}
public MicrosoftAccountLoginPane(boolean bodyonly) {
this(null, null, null, bodyonly);
}
public MicrosoftAccountLoginPane(Account account, Consumer<AuthInfo> callback, Runnable onCancel, boolean bodyonly) {
this.accountToRelogin = account;
this.loginCallback = callback;
this.cancelCallback = onCancel;
getStyleClass().add("microsoft-login-dialog");
if (bodyonly) {
this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true);
} else {
Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft"));
heading.getStyleClass().add("header-label");
setHeading(heading);
}
this.setMaxWidth(650);
onEscPressed(this, this::onCancel);
btnLogin = new JFXButton(i18n("account.login"));
btnLogin.getStyleClass().add("dialog-accept");
loginButtonSpinner = new SpinnerPane();
loginButtonSpinner.getStyleClass().add("small-spinner-pane");
loginButtonSpinner.setContent(btnLogin);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
btnCancel.setOnAction(e -> onCancel());
setActions(loginButtonSpinner, btnCancel);
holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartAuthorizationCodeLogin)
step.set(new Step.WaitForOpenBrowser(event.getUrl()));
}));
holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartDeviceCodeLogin)
step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri()));
}));
this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty()
? new Step.Init()
: new Step.StartAuthorizationCodeLogin());
FXUtils.onChangeAndOperate(step, this::onStep);
}
private void onStep(Step currentStep) {
VBox rootContainer = new VBox(10);
setBody(rootContainer);
rootContainer.setAlignment(Pos.TOP_CENTER);
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
rootContainer.getChildren().add(snapshotHint);
btnLogin.setDisable(true);
loginButtonSpinner.setLoading(false);
return;
}
if (!IntegrityChecker.isOfficial()) {
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
unofficialHintPane.setSegment(i18n("unofficial.hint"));
rootContainer.getChildren().add(unofficialHintPane);
}
if (currentStep instanceof Step.Init) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
var hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("account.methods.microsoft.hint"));
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
loginButtonSpinner.setLoading(false);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.StartDeviceCodeLogin) {
loginButtonSpinner.setLoading(true);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.WaitForOpenBrowser wait) {
btnLogin.setOnAction(e -> {
FXUtils.openLink(wait.url());
loginButtonSpinner.setLoading(true);
});
loginButtonSpinner.setLoading(false);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setSegment(
i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()),
FXUtils::copyText
);
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
loginButtonSpinner.setLoading(true);
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
? "https://www.microsoft.com/link?otc=" + wait.userCode()
: wait.verificationUri();
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
StringUtils.escapeXmlAttribute(scanUri),
wait.verificationUri(),
wait.userCode()
));
var qrCode = new SVGPath();
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(scanUri, QrCode.Ecc.MEDIUM)));
qrCode.setScaleX(3);
qrCode.setScaleY(3);
var lblCode = new Label(wait.userCode());
lblCode.getStyleClass().add("code-label");
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
var codeBox = new StackPane(lblCode);
codeBox.getStyleClass().add("code-box");
codeBox.setCursor(Cursor.HAND);
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
codeBox.setMaxWidth(USE_PREF_SIZE);
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
} else if (currentStep instanceof Step.LoginFailed failed) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
cancelAllTasks();
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setSegment(failed.message());
rootContainer.getChildren().add(errHintPane);
}
var linkBox = new FlowPane(8, 8);
linkBox.setAlignment(Pos.CENTER_LEFT);
linkBox.setPrefWrapLength(500);
if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) {
JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device"));
useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin()));
linkBox.getChildren().add(useQrCode);
} else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) {
JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser"));
userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
linkBox.getChildren().add(userBrowser);
}
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
linkBox.getChildren().addAll(profileLink, purchaseLink);
rootContainer.getChildren().add(linkBox);
setBody(rootContainer);
}
private void cancelAllTasks() {
if (browserTaskExecutor != null) browserTaskExecutor.cancel();
if (deviceTaskExecutor != null) deviceTaskExecutor.cancel();
}
private void onCancel() {
cancelAllTasks();
if (cancelCallback != null) cancelCallback.run();
fireEvent(new DialogCloseEvent());
}
private void onLoginCompleted(MicrosoftAccount account, Exception exception) {
if (exception == null) {
if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin);
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
Accounts.setSelectedAccount(account);
if (loginCallback != null) {
try {
loginCallback.accept(account.logIn());
} catch (AuthenticationException e) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e)));
return;
}
}
fireEvent(new DialogCloseEvent());
} else if (!(exception instanceof CancellationException)) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception)));
}
}
private sealed interface Step {
final class Init implements Step {
}
final class StartAuthorizationCodeLogin implements Step {
}
record WaitForOpenBrowser(String url) implements Step {
}
final class StartDeviceCodeLogin implements Step {
}
record WaitForScanQrCode(String userCode, String verificationUri) implements Step {
}
record LoginFailed(String message) implements Step {
}
}
}
@@ -0,0 +1,109 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.OAuthAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OAuthAccountLoginDialog extends DialogPane {
private final OAuthAccount account;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
this.account = account;
this.success = success;
this.failed = failed;
setTitle(i18n("account.login.refresh"));
VBox vbox = new VBox(8);
Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
);
} else {
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.hint")
);
}
});
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(usernameLabel, hintPane, box);
setBody(vbox);
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override
protected void onAccept() {
setLoading();
Task.supplyAsync(account::logInWhenCredentialsExpired)
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
if (exception == null) {
success.accept(authInfo);
onSuccess();
} else {
LOG.info("Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
}
@Override
protected void onCancel() {
failed.run();
super.onCancel();
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
@@ -27,8 +28,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import java.util.function.Consumer;
@@ -99,17 +100,27 @@ public class AdvancedListBox extends ScrollPane {
return add(item);
}
@SuppressWarnings("SuspiciousNameCombination")
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
SVG unselectedGraphic, SVG selectedGraphic) {
AdvancedListItem item = createNavigationDrawerItem(title, null);
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setMouseTransparent(true);
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
TransitionPane leftGraphic = new TransitionPane();
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
leftGraphic.setMouseTransparent(true);
leftGraphic.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setPadding(Insets.EMPTY);
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
FXUtils.onChange(item.activeProperty(), active ->
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
item.setLeftGraphic(leftGraphic);
return add(item);
}
@@ -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();
@@ -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;
}
};
}
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Locale;
import java.util.function.Consumer;
public class HintPane extends VBox {
private final Text label = new Text();
@@ -67,11 +66,7 @@ public class HintPane extends VBox {
}
public void setSegment(String segment) {
this.setSegment(segment, Controllers::onHyperlinkAction);
}
public void setSegment(String segment, Consumer<String> hyperlinkAction) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction));
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
}
public void setChildren(Node... children) {
@@ -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;
}
}
@@ -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;
@@ -83,8 +81,6 @@ public final 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 final 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();
}
}
}
}
@@ -31,12 +31,16 @@ import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends HBox {
@@ -68,7 +72,7 @@ public final class MessageDialogPane extends HBox {
private @Nullable ButtonBase cancelButton;
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
this.setSpacing(16);
this.setSpacing(8);
this.getStyleClass().add("jfx-dialog-layout");
Label graphic = new Label();
@@ -215,6 +219,36 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder cancelOnTimeout(long timeoutMs) {
if (dialog.getCancelButton() == null) {
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
}
ButtonBase cancelButton = dialog.getCancelButton();
String originalText = cancelButton.getText();
Timer timer = Lang.getTimer();
timer.scheduleAtFixedRate(new TimerTask() {
long timeout = timeoutMs;
@Override
public void run() {
if (timeout <= 0) {
cancel();
runInFX(() -> {
cancelButton.fire();
});
return;
}
timeout -= 1000;
long currentTimeout = timeout;
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
}
}, 1000, 1000);
return this;
}
public MessageDialogPane build() {
return dialog;
}
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -31,12 +33,14 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
/// A spinner pane that can show spinner, failed reason, or content.
@DefaultProperty("content")
public class SpinnerPane extends Control {
private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public SpinnerPane() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
getStyleClass().add("spinner-pane");
}
public void showSpinner() {
@@ -48,196 +52,108 @@ public class SpinnerPane extends Control {
setLoading(false);
}
private void updateContent() {
if (getSkin() instanceof Skin skin) {
skin.updateContent();
}
public Node getContent() {
return content.get();
}
private ObjectProperty<Node> content;
public ObjectProperty<Node> contentProperty() {
if (content == null)
content = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "content";
}
@Override
protected void invalidated() {
updateContent();
}
};
return content;
}
public Node getContent() {
return contentProperty().get();
}
public void setContent(Node content) {
contentProperty().set(content);
}
private BooleanProperty loading;
public BooleanProperty loadingProperty() {
if (loading == null)
loading = new BooleanPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "loading";
}
@Override
protected void invalidated() {
updateContent();
}
};
return loading;
this.content.set(content);
}
public boolean isLoading() {
return loading != null && loading.get();
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
loadingProperty().set(loading);
}
private StringProperty failedReason;
public StringProperty failedReasonProperty() {
if (failedReason == null)
failedReason = new StringPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "failedReason";
}
@Override
protected void invalidated() {
updateContent();
}
};
return failedReason;
this.loading.set(loading);
}
public String getFailedReason() {
return failedReason != null ? failedReason.get() : null;
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
failedReasonProperty().set(failedReason);
this.failedReason.set(failedReason);
}
private ObjectProperty<EventHandler<Event>> onFailedAction;
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
if (onFailedAction == null) {
onFailedAction = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "onFailedAction";
}
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
}
return onFailedAction;
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedAction != null ? onFailedAction.get() : null;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected SkinBase<SpinnerPane> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
Skin(SpinnerPane control) {
super(control);
root.setClip(null);
updateContent();
this.getChildren().setAll(root);
}
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onClicked(failedPane, () -> {
EventHandler<Event> action = control.getOnFailedAction();
if (action != null)
action.handle(new Event(FAILED_ACTION));
});
private StackPane contentPane;
private StackPane spinnerPane;
private StackPane failedPane;
private Label failedReasonLabel;
void updateContent() {
SpinnerPane control = getSkinnable();
Node nextContent;
if (control.isLoading()) {
if (spinnerPane == null) {
spinnerPane = new StackPane(new JFXSpinner());
spinnerPane.getStyleClass().add("notice-pane");
}
nextContent = spinnerPane;
} else if (control.getFailedReason() != null) {
if (failedPane == null) {
failedReasonLabel = new Label();
failedPane = new StackPane(failedReasonLabel);
failedPane.getStyleClass().add("notice-pane");
FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
}
failedReasonLabel.setText(control.getFailedReason());
nextContent = failedPane;
} else {
if (contentPane == null) {
contentPane = new StackPane();
}
Node content = control.getContent();
if (content != null)
contentPane.getChildren().setAll(content);
else
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
nextContent = contentPane;
}
if (nextContent != failedPane && failedReasonLabel != null) {
failedReasonLabel.setText(null);
}
root.setContent(nextContent, ContainerAnimations.FADE);
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE);
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
@@ -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() {
@@ -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;
@@ -57,12 +62,13 @@ 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);
}
@@ -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;
}
@@ -84,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"));
@@ -94,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);
@@ -129,7 +129,7 @@ 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();
@@ -138,7 +138,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
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 -> {
@@ -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());
}
}
@@ -42,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);
@@ -139,11 +139,13 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
} else {
// 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 -> {
@@ -154,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}),
}).executor(true),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -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
@@ -162,8 +162,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
RemoteVersionListCell(VersionsPage control) {
this.control = control;
imageView.setMouseTransparent(true);
HBox hbox = new HBox(16);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
hbox.setAlignment(Pos.CENTER);
@@ -278,12 +276,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,
@@ -18,13 +18,13 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
@@ -48,8 +48,6 @@ 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.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -110,39 +108,23 @@ public final class JavaDownloadDialog extends StackPane {
}
private final class DownloadMojangJava extends DialogPane {
private final List<JFXCheckBox> checkboxes = new ArrayList<>();
private final ToggleGroup toggleGroup = new ToggleGroup();
DownloadMojangJava() {
setTitle(i18n("java.download.title"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
setValid(false);
InvalidationListener updateValidStatus = e -> {
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
setValid(true);
return;
}
}
setValid(false);
};
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXCheckBox button = new JFXCheckBox("Java " + version.majorVersion());
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
button.setUserData(version);
if (JavaManager.REPOSITORY.isInstalled(platform, version)) {
button.setDisable(true);
button.setSelected(true);
} else {
button.selectedProperty().addListener(updateValidStatus);
}
vbox.getChildren().add(button);
checkboxes.add(button);
toggleGroup.getToggles().add(button);
if (JavaManager.REPOSITORY.isInstalled(platform, version))
button.setDisable(true);
}
setBody(vbox);
@@ -155,52 +137,38 @@ public final class JavaDownloadDialog extends StackPane {
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
List<GameJavaVersion> selectedVersions = new ArrayList<>();
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
selectedVersions.add((GameJavaVersion) box.getUserData());
}
}
if (selectedVersions.isEmpty())
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
return;
Task<Void> task = Task.allOf(selectedVersions.stream().map(javaVersion -> JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).wrapResult()).toList())
.whenComplete(Schedulers.javafx(), (results, exception) -> {
Throwable exceptionToDisplay;
if (exception == null) {
List<Throwable> exceptions = results.stream()
.filter(Result::isFailure)
.map(Result::getException)
.map(Lang::resolveException)
.filter(it -> !(it instanceof CancellationException))
.toList();
if (!exceptions.isEmpty()) {
if (exceptions.size() == 1) {
exceptionToDisplay = exceptions.get(0);
} else {
exceptionToDisplay = new IOException("Failed to download Java");
for (Throwable e : exceptions)
exceptionToDisplay.addSuppressed(e);
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
}
} else {
exceptionToDisplay = null;
}
} else {
exceptionToDisplay = exception;
}
if (exceptionToDisplay != null) {
LOG.warning("Failed to download java", exceptionToDisplay);
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
}
});
Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL);
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL);
}
}
@@ -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"));
@@ -45,7 +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.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
@@ -141,8 +140,10 @@ public final class SettingsPage extends ScrollPane {
}
{
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
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"));
updateListener = any -> {
@@ -217,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(
@@ -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);
@@ -15,7 +15,6 @@ import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -171,10 +170,6 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
rendererPane.setDescriptionConverter(e -> {
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setItems(Renderer.values());
noJVMArgsPane = new LineToggleButton();
@@ -21,7 +21,6 @@ import javafx.beans.property.BooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -45,13 +44,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
private final World world;
private final Path worldDir;
private final Datapack datapack;
final BooleanProperty readOnly;
public DatapackListPage(WorldManagePage worldManagePage) {
world = worldManagePage.getWorld();
datapack = new Datapack(world.getFile().resolve("datapacks"));
this.worldDir = worldManagePage.getWorld().getFile();
datapack = new Datapack(worldDir.resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
@@ -69,7 +68,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
private void installSingleDatapack(Path datapack) {
try {
this.datapack.installPack(datapack, world.getGameVersion());
this.datapack.installPack(datapack);
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse datapack file " + datapack, e);
}
@@ -35,6 +35,7 @@ import javafx.scene.Node;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
@@ -49,7 +50,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.Nullable;
@@ -110,7 +114,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
);
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
@@ -270,20 +274,20 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageContainer.setImage(image);
imageView.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
@@ -292,7 +296,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return;
}
}
imageContainer.setImage(image);
imageView.setImage(image);
}, Schedulers.javafx());
}
@@ -300,7 +304,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(32);
ImageView imageView = new ImageView();
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
@@ -316,10 +320,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
checkBox.disableProperty().bind(isReadOnlyProperty);
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageView.setFitWidth(32);
imageView.setFitHeight(32);
imageView.setPreserveRatio(true);
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
StackPane.setMargin(container, new Insets(8));
container.getChildren().setAll(checkBox, imageContainer, content);
container.getChildren().setAll(checkBox, imageView, content);
getContainer().getChildren().setAll(container);
getContainer().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(this, mouseEvent));
@@ -334,7 +341,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty);
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
}
}
@@ -17,10 +17,8 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.*;
@@ -34,6 +32,7 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
@@ -49,7 +48,10 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -101,10 +103,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
this.downloadProvider = DownloadProviders.getDownloadProvider();
}
public DownloadProvider getDownloadProvider() {
return downloadProvider;
}
public ObservableList<Node> getActions() {
return actions;
}
@@ -237,20 +235,18 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
private final JFXListView<RemoteMod> listView = new JFXListView<>();
private final RemoteImageLoader iconLoader;
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
@Override
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
}
};
protected ModDownloadListPageSkin(DownloadListPage control) {
super(control);
listView.getStyleClass().add("no-horizontal-scrollbar");
iconLoader = new RemoteImageLoader(control.downloadProvider) {
@Override
protected @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris) {
return FXUtils.getRemoteImageTask(uris, 80, 80, true, true);
}
};
BorderPane pane = new BorderPane();
GridPane searchPane = new GridPane();
@@ -536,29 +532,28 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private static final Insets PADDING = new Insets(9, 9, 0, 9);
private final RipplerContainer graphic;
private final StackPane wrapper = new StackPane();
private final TwoLineListItem content = new TwoLineListItem();
private final ImageContainer imageContainer = new ImageContainer(40);
private final ImageView imageView = new ImageView();
{
setPadding(PADDING);
HBox container = new HBox(8);
container.setPadding(new Insets(8));
container.getStyleClass().add("card");
container.setCursor(Cursor.HAND);
container.setAlignment(Pos.CENTER_LEFT);
JFXDepthManager.setDepth(container, 1);
imageContainer.setMouseTransparent(true);
imageView.setFitWidth(40);
imageView.setFitHeight(40);
container.getChildren().setAll(imageContainer, content);
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
HBox.setHgrow(content, Priority.ALWAYS);
this.graphic = new RipplerContainer(container);
wrapper.getChildren().setAll(this.graphic);
wrapper.getStyleClass().add("card-no-padding");
FXUtils.onClicked(wrapper, () -> {
graphic.setPosition(JFXRippler.RipplerPos.FRONT);
FXUtils.onClicked(graphic, () -> {
RemoteMod item = getItem();
if (item != null)
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getProfileVersion(), getSkinnable().callback));
@@ -584,8 +579,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
if (getSkinnable().shouldDisplayCategory(category))
content.addTag(getSkinnable().getLocalizedCategory(category));
}
iconLoader.load(imageContainer.imageProperty(), item.getIconUrl());
setGraphic(wrapper);
iconLoader.load(imageView.imageProperty(), item.getIconUrl());
setGraphic(graphic);
}
}
});
@@ -32,13 +32,14 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
@@ -113,7 +114,7 @@ public class DownloadPage extends Control implements DecoratorPage {
setFailed(false);
Task.supplyAsync(() -> {
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository, page.getDownloadProvider());
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
return sortVersions(versions);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@@ -180,7 +181,7 @@ public class DownloadPage extends Control implements DecoratorPage {
if (this.callback == null) {
saveAs(mod, file);
} else {
this.callback.download(page.getDownloadProvider(), version.getProfile(), version.getVersion(), mod, file);
this.callback.download(version.getProfile(), version.getVersion(), mod, file);
}
}
@@ -224,18 +225,25 @@ public class DownloadPage extends Control implements DecoratorPage {
VBox pane = new VBox(8);
pane.getStyleClass().add("gray-background");
pane.setPadding(new Insets(10));
ScrollPane scrollPane = new ScrollPane(pane);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
HBox descriptionPane = new HBox(8);
descriptionPane.setMinHeight(Region.USE_PREF_SIZE);
descriptionPane.setAlignment(Pos.CENTER);
pane.getChildren().add(descriptionPane);
descriptionPane.getStyleClass().add("card-non-transparent");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
{
var imageContainer = new ImageContainer(40);
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
imageContainer.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
}
descriptionPane.getChildren().add(imageContainer);
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
@@ -253,12 +261,14 @@ public class DownloadPage extends Control implements DecoratorPage {
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
descriptionPane.getChildren().add(openMcmodButton);
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod"));
}
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
descriptionPane.getChildren().add(openUrlButton);
openUrlButton.setMinWidth(Region.USE_PREF_SIZE);
FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage());
}
SpinnerPane spinnerPane = new SpinnerPane();
@@ -276,13 +286,8 @@ public class DownloadPage extends Control implements DecoratorPage {
spinnerPane.setOnFailedAction(e -> getSkinnable().loadModVersions());
ComponentList list = new ComponentList();
ScrollPane scrollPane = new ScrollPane(list);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.setOverflowHidden(scrollPane, 8);
StackPane.setAlignment(scrollPane, Pos.TOP_CENTER);
spinnerPane.setContent(scrollPane);
StackPane.setAlignment(list, Pos.TOP_CENTER);
spinnerPane.setContent(list);
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
if (control.versions == null) return;
@@ -336,7 +341,7 @@ public class DownloadPage extends Control implements DecoratorPage {
});
}
getChildren().setAll(pane);
getChildren().setAll(scrollPane);
}
}
@@ -357,8 +362,10 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
var imageView = new ImageContainer(40);
pane.getChildren().setAll(imageView, content);
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
RipplerContainer container = new RipplerContainer(pane);
FXUtils.onClicked(container, () -> {
@@ -486,7 +493,6 @@ public class DownloadPage extends Control implements DecoratorPage {
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.smoothScrolling(scrollPane);
FXUtils.setOverflowHidden(scrollPane, 8);
spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
@@ -549,7 +555,7 @@ public class DownloadPage extends Control implements DecoratorPage {
dependencies.put(dependency.getType(), list);
}
queue.add(Task.supplyAsync(Schedulers.io(), () -> dependency.load(selfPage.page.getDownloadProvider()))
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
.setSignificance(Task.TaskSignificance.MINOR)
.thenAcceptAsync(Schedulers.javafx(), dep -> {
if (dep == RemoteMod.BROKEN) {
@@ -577,8 +583,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
@FunctionalInterface
public interface DownloadCallback {
void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
void download(Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
}
}
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -25,26 +27,32 @@ import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final ImageContainer imageContainer;
private final ImageView imageView;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings("SuspiciousNameCombination")
public GameAdvancedListItem() {
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
imageContainer.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
this.imageView = new ImageView();
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setPreserveRatio(true);
imageView.setImage(null);
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
setLeftGraphic(imageViewWrapper);
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
}
private void loadVersion(String version) {
@@ -60,11 +68,11 @@ public class GameAdvancedListItem extends AdvancedListItem {
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
setTitle(i18n("version.manage.manage"));
setSubtitle(version);
imageContainer.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageContainer.setImage(VersionIconType.DEFAULT.getIcon());
imageView.setImage(VersionIconType.DEFAULT.getIcon());
}
}
}
@@ -25,6 +25,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.ListCell;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -41,7 +42,7 @@ public final class GameListCell extends ListCell<GameListItem> {
private final Region graphic;
private final ImageContainer imageView;
private final ImageView imageView;
private final TwoLineListItem content;
private final JFXRadioButton chkSelected;
@@ -86,7 +87,8 @@ public final class GameListCell extends ListCell<GameListItem> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
this.imageView = new ImageContainer(32);
this.imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
this.content = new TwoLineListItem();
BorderPane.setAlignment(content, Pos.CENTER);
@@ -106,26 +108,30 @@ public final class GameListCell extends ListCell<GameListItem> {
right.setAlignment(Pos.CENTER_RIGHT);
this.btnUpgrade = FXUtils.newToggleButton4(SVG.UPDATE);
this.btnUpgrade = new JFXButton();
btnUpgrade.setOnAction(e -> {
GameListItem item = this.getItem();
if (item != null)
item.update();
});
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.UPDATE.createIcon());
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
right.getChildren().add(btnUpgrade);
this.btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH);
this.btnLaunch = new JFXButton();
btnLaunch.setOnAction(e -> {
GameListItem item = this.getItem();
if (item != null)
item.testGame();
});
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
right.getChildren().add(btnLaunch);
this.btnManage = FXUtils.newToggleButton4(SVG.MORE_VERT);
this.btnManage = new JFXButton();
btnManage.setOnAction(e -> {
GameListItem item = this.getItem();
if (item == null)
@@ -135,7 +141,9 @@ public final class GameListCell extends ListCell<GameListItem> {
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(SVG.MORE_VERT.createIcon());
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
right.getChildren().add(btnManage);
}
@@ -17,34 +17,21 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
@@ -52,14 +39,9 @@ import org.jackhuang.hmcl.ui.profile.ProfilePage;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
@@ -78,6 +60,8 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
});
selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class);
GameList gameList = new GameList();
{
ScrollPane pane = new ScrollPane();
VBox.setVgrow(pane, Priority.ALWAYS);
@@ -101,12 +85,13 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame)
.addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack)
.addNavigationDrawerItem(i18n("button.refresh"), SVG.REFRESH, gameList::refreshList)
.addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2);
setLeft(pane, bottomLeftCornerList);
}
setCenter(new GameList());
setCenter(gameList);
}
public ObjectProperty<Profile> selectedProfileProperty() {
@@ -137,12 +122,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
private static class GameList extends ListPageBase<GameListItem> {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final ObservableList<GameListItem> sourceList = FXCollections.observableArrayList();
private final FilteredList<GameListItem> filteredList = new FilteredList<>(sourceList);
public GameList() {
setItems(filteredList);
Profiles.registerVersionsListener(this::loadVersions);
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
@@ -153,125 +133,44 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
listenerHolder.clear();
setLoading(true);
setFailedReason(null);
if (profile != Profiles.getSelectedProfile())
return;
List<GameListItem> versionItems = profile.getRepository().getDisplayVersions().map(instance -> new GameListItem(profile, instance.getId())).toList();
sourceList.setAll(versionItems);
if (versionItems.isEmpty()) {
ObservableList<GameListItem> children = FXCollections.observableList(profile.getRepository().getDisplayVersions()
.map(instance -> new GameListItem(profile, instance.getId()))
.toList());
setItems(children);
if (children.isEmpty()) {
setFailedReason(i18n("version.empty.hint"));
}
setLoading(false);
}
private Predicate<GameListItem> createPredicate(String searchText) {
if (searchText == null || searchText.isEmpty()) {
return item -> true;
}
if (searchText.startsWith("regex:")) {
String regex = searchText.substring("regex:".length());
try {
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
return item -> pattern.matcher(item.id).find();
} catch (PatternSyntaxException e) {
return item -> false;
}
} else {
return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT));
}
}
public void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
@Override
protected Skin<?> createDefaultSkin() {
return new GameListSkin(this);
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
private static class GameListSkin extends SkinBase<GameList> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
private final HBox toolbarNormal;
private class GameListSkin extends ToolbarListPageSkin<GameListItem, GameList> {
private final JFXTextField searchField;
public GameListSkin(GameList skinnable) {
super(skinnable);
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<GameListItem> listView = new JFXListView<>();
{
toolbarPane = new TransitionPane();
searchBar = new HBox();
toolbarNormal = new HBox();
searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
changeToolbar(toolbarNormal);
searchField.clear();
});
onEscPressed(searchField, closeSearchBar::fire);
searchBar.getChildren().setAll(searchField, closeSearchBar);
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
root.getContent().add(toolbarPane);
}
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
center.failedReasonProperty().bind(skinnable.failedReasonProperty());
listView.setCellFactory(x -> new GameListCell());
listView.setItems(skinnable.getItems());
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
center.setContent(listView);
root.getContent().add(center);
}
pane.getChildren().setAll(root);
getChildren().setAll(pane);
public GameListSkin() {
super(GameList.this);
}
private void changeToolbar(HBox newToolbar) {
Node oldToolbar = toolbarPane.getCurrentNode();
if (newToolbar != oldToolbar) {
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
if (newToolbar == searchBar) {
runInFX(searchField::requestFocus);
}
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
@Override
protected ListCell<GameListItem> createListCell(JFXListView<GameListItem> listView) {
return new GameListCell();
}
}
}
}
@@ -30,6 +30,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
@@ -37,7 +38,6 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
@@ -87,7 +87,7 @@ public final class GameListPopupMenu extends StackPane {
private final Region graphic;
private final ImageContainer imageView;
private final ImageView imageView;
private final TwoLineListItem content;
private final StringProperty tag = new SimpleStringProperty();
@@ -103,7 +103,8 @@ public final class GameListPopupMenu extends StackPane {
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
this.imageView = new ImageContainer(32);
this.imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageViewContainer.getChildren().setAll(imageView);
this.content = new TwoLineListItem();
@@ -50,9 +50,7 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
}
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, CurseForgeRemoteModRepository.SHADERS, ModrinthRemoteModRepository.SHADER_PACKS);
page.supportChinese.set(false);
return page;
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
}
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.Schedulers;
@@ -31,18 +30,16 @@ import java.util.Objects;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
private final DownloadProvider downloadProvider;
private final List<Task<LocalModFile.ModUpdate>> dependents;
public ModCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<LocalModFile> mods) {
this.downloadProvider = downloadProvider;
public ModCheckUpdatesTask(String gameVersion, Collection<LocalModFile> mods) {
dependents = mods.stream().map(mod ->
Task.supplyAsync(Schedulers.io(), () -> {
LocalModFile.ModUpdate candidate = null;
for (RemoteMod.Type type : RemoteMod.Type.values()) {
LocalModFile.ModUpdate update = null;
try {
update = mod.checkUpdates(downloadProvider, gameVersion, type.getRemoteModRepository());
update = mod.checkUpdates(gameVersion, type.getRemoteModRepository());
} catch (IOException e) {
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
}
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -44,7 +43,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
@@ -241,12 +239,11 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
if (gameVersion.isPresent()) {
return new ModCheckUpdatesTask(DownloadProviders.getDownloadProvider(), gameVersion.get(), mods);
return new ModCheckUpdatesTask(gameVersion.get(), mods);
}
return null;
})
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception instanceof CancellationException) return;
if (exception != null || result == null) {
Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
} else if (result.isEmpty()) {
@@ -255,7 +252,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
Controllers.navigateForward(new ModUpdatesPage(modManager, result));
}
})
.withStagesHints("update.checking"),
.withStagesHint(Collections.singletonList("update.checking")),
i18n("mods.check_updates"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(instanceId)) {
@@ -30,6 +30,7 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
@@ -43,7 +44,6 @@ import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
@@ -155,7 +155,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
// Toolbar Selecting
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
@@ -377,20 +377,20 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
}
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageContainer.setImage(image);
imageView.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
imageContainer.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
@@ -400,7 +400,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
}
imageContainer.setImage(image);
imageView.setImage(image);
}, Schedulers.javafx());
}
}
@@ -414,8 +414,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
var imageContainer = new ImageContainer(40);
modInfo.loadIcon(imageContainer, null);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 40, 40);
modInfo.loadIcon(imageView, null);
TwoLineListItem title = new TwoLineListItem();
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
@@ -436,7 +437,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
title.setSubtitle(subtitle.toString());
titleContainer.getChildren().setAll(imageContainer, title);
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
setHeading(titleContainer);
Label description = new Label(modInfo.getModInfo().getDescription().toString());
@@ -466,7 +467,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Task.runAsync(() -> {
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
if (versionOptional.isPresent()) {
RemoteMod remoteMod = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().getModid());
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
FXUtils.runInFX(() -> {
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
String loaderName = switch (modLoaderType) {
@@ -494,7 +495,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
remoteMod,
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
(downloadProvider, profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, profile, version, file, "mods")
(profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
));
});
button.setDisable(false);
@@ -553,11 +554,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(24);
ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
JFXButton restoreButton = new JFXButton();
JFXButton infoButton = new JFXButton();
JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty;
Tooltip warningTooltip;
@@ -574,11 +575,23 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
content.setMouseTransparent(true);
setSelectable();
imageContainer.setImage(VersionIconType.COMMAND.getIcon());
imageView.setFitWidth(24);
imageView.setFitHeight(24);
imageView.setPreserveRatio(true);
imageView.setImage(VersionIconType.COMMAND.getIcon());
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(SVG.RESTORE.createIcon());
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
container.getChildren().setAll(checkBox, imageContainer, content, restoreButton, revealButton, infoButton);
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(SVG.FOLDER.createIcon());
infoButton.getStyleClass().add("toggle-icon4");
infoButton.setGraphic(SVG.INFO.createIcon());
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
StackPane.setMargin(container, new Insets(8));
getContainer().getChildren().setAll(container);
@@ -603,7 +616,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ModLoaderType modLoaderType = modInfo.getModLoaderType();
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
String displayName = modInfo.getName();
if (modTranslations != null && I18n.isUseChinese()) {
@@ -8,6 +8,7 @@ import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -18,7 +19,10 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream;
@@ -177,10 +181,10 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
}
private static final class ResourcepackListCell extends MDListCell<ResourcepackInfoObject> {
private final ImageContainer imageView = new ImageContainer(32);
private final ImageView imageView = new ImageView();
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
private final JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
private final JFXButton btnReveal = new JFXButton();
private final JFXButton btnDelete = new JFXButton();
private final ResourcepackListPage page;
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, ResourcepackListPage page) {
@@ -194,6 +198,7 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
HBox left = new HBox(8);
left.setAlignment(Pos.CENTER);
FXUtils.limitSize(imageView, 32, 32);
left.getChildren().add(imageView);
left.setPadding(new Insets(0, 8, 0, 0));
FXUtils.setLimitWidth(left, 48);
@@ -202,6 +207,12 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
HBox.setHgrow(content, Priority.ALWAYS);
root.setCenter(content);
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
HBox right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
right.getChildren().setAll(btnReveal, btnDelete);
@@ -22,14 +22,17 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
@@ -441,7 +444,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
if (image == null) {
return super.getIcon(size);
} else {
var imageView = new ImageContainer(size);
ImageView imageView = new ImageView();
imageView.setFitHeight(size);
imageView.setFitWidth(size);
imageView.setImage(image);
return imageView;
}
@@ -545,8 +550,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final TwoLineListItem center;
private final HBox right;
private final ImageContainer iconImageView;
private final SVGContainer iconSVGView;
private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;
private final Tooltip tooltip = new Tooltip();
@@ -559,8 +565,18 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.left = new StackPane();
left.setPadding(new Insets(0, 8, 0, 0));
this.iconImageView = new ImageContainer(32);
this.iconSVGView = new SVGContainer(32);
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);
this.iconSVG = new SVGPath();
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);
BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
@@ -575,15 +591,19 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
JFXButton btnReveal = new JFXButton();
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem))
item.onReveal();
});
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
JFXButton btnDelete = new JFXButton();
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
btnDelete.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem)) {
@@ -618,8 +638,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVGView.setIcon(item.getIcon());
left.getChildren().setAll(iconSVGView);
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
}
center.setTitle(item.getName());
@@ -21,8 +21,6 @@ import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -63,13 +61,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private String preferredVersionName = null;
public static class WorkingDirChangedEvent extends Event {
public static final EventType<WorkingDirChangedEvent> EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED");
public WorkingDirChangedEvent() {
super(EVENT_TYPE);
}
}
public VersionPage() {
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
@@ -83,21 +74,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> {
if (this.version.get() != null) {
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(getProfile(), getVersion());
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(getProfile(), getVersion());
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(getProfile(), getVersion());
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(getProfile(), getVersion());
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(getProfile(), getVersion());
}
});
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
}
@@ -34,12 +34,8 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -52,6 +48,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
@@ -102,10 +99,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final InvalidationListener usesGlobalListener;
private final ChangeListener<Boolean> specificSettingsListener;
private final InvalidationListener javaListener = any -> initJavaSubtitle();
private final ChangeListener<GameDirectoryType> gameDirTypeListener = (ob, o, n) -> fireWorkingDirChanged();
private final ChangeListener<String> gameDirListener = (ob, o, n) -> fireWorkingDirChanged();
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
@@ -462,20 +455,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
else
profile.getRepository().globalizeVersionSetting(versionId);
FXUtils.runInFX(() -> {
loadVersion(profile, versionId);
fireWorkingDirChanged();
});
Platform.runLater(() -> loadVersion(profile, versionId));
};
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
componentList.disableProperty().bind(enableSpecificSettings.not());
}
private void fireWorkingDirChanged() {
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
}
@Override
public void loadVersion(Profile profile, String versionId) {
@@ -527,9 +513,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting.defaultJavaPathPropertyProperty().removeListener(javaListener);
lastVersionSetting.javaVersionProperty().removeListener(javaListener);
lastVersionSetting.gameDirTypeProperty().removeListener(gameDirTypeListener);
lastVersionSetting.gameDirProperty().removeListener(gameDirListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().unbind();
@@ -606,9 +589,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> profile.getRepository().getRunDirectory(versionId).toAbsolutePath().normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
versionSetting.gameDirTypeProperty().addListener(gameDirTypeListener);
versionSetting.gameDirProperty().addListener(gameDirListener);
lastVersionSetting = versionSetting;
initJavaSubtitle();
@@ -747,6 +727,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
private static List<String> getSupportedResolutions() {
@@ -22,7 +22,6 @@ import javafx.application.Platform;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.RemoteMod;
@@ -43,13 +42,13 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -73,11 +72,11 @@ public final class Versions {
}
}
public static void downloadModpackImpl(DownloadProvider downloadProvider, Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
public static void downloadModpackImpl(Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
Path modpack;
List<URI> downloadURLs;
URI downloadURL;
try {
downloadURLs = downloadProvider.injectURLWithCandidates(file.getFile().getUrl());
downloadURL = NetworkUtils.toURI(file.getFile().getUrl());
modpack = Files.createTempFile("modpack", ".zip");
} catch (IOException | IllegalArgumentException e) {
Controllers.dialog(
@@ -86,7 +85,7 @@ public final class Versions {
return;
}
Controllers.taskDialog(
new FileDownloadTask(downloadURLs, modpack)
new FileDownloadTask(downloadURL, modpack)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
ModpackInstallWizardProvider installWizardProvider;
@@ -104,7 +103,7 @@ public final class Versions {
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
}
}),
}).executor(true),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -26,6 +26,7 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -34,7 +35,6 @@ import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
@@ -232,8 +232,9 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
var imageView = new ImageContainer(32);
ImageView imageView = new ImageView();
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
@@ -254,14 +255,18 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
JFXButton btnReveal = new JFXButton();
right.getChildren().add(btnReveal);
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> skinnable.onReveal());
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
JFXButton btnDelete = new JFXButton();
right.getChildren().add(btnDelete);
FXUtils.installFastTooltip(btnDelete, i18n("world.backup.delete"));
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE.createIcon());
btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::onDelete, null));
}
@@ -30,11 +30,14 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -64,10 +67,9 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
private final WorldManagePage worldManagePage;
private boolean isReadOnly;
private final World world;
private CompoundTag levelData;
private CompoundTag playerData;
private CompoundTag levelDat;
private final ImageContainer iconImageView = new ImageContainer(32);
ImageView iconImageView = new ImageView();
public WorldInfoPage(WorldManagePage worldManagePage) {
this.worldManagePage = worldManagePage;
@@ -75,9 +77,15 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
refresh();
}
private CompoundTag loadWorldInfo() throws IOException {
if (!Files.isDirectory(world.getFile()))
throw new IOException("Not a valid world directory");
return world.getLevelData();
}
private void updateControls() {
CompoundTag dataTag = levelData.get("Data");
CompoundTag playerTag = playerData;
CompoundTag dataTag = levelDat.get("Data");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -129,12 +137,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
iconPane.setTitle(i18n("world.icon"));
{
FXUtils.limitSize(iconImageView, 32, 32);
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
JFXButton editIconButton = FXUtils.newToggleButton4(SVG.EDIT, 20);
JFXButton resetIconButton = FXUtils.newToggleButton4(SVG.RESTORE, 20);
JFXButton editIconButton = new JFXButton();
JFXButton resetIconButton = new JFXButton();
{
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
editIconButton.setDisable(isReadOnly);
editIconButton.setOnAction(event -> Controllers.confirm(
I18n.i18n("world.icon.change.tip"),
@@ -144,10 +154,13 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
null
));
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
editIconButton.getStyleClass().add("toggle-icon4");
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
resetIconButton.setDisable(isReadOnly);
resetIconButton.setOnAction(event -> this.clearWorldIcon());
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
resetIconButton.getStyleClass().add("toggle-icon4");
}
HBox hBox = new HBox(8);
@@ -198,14 +211,12 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
worldSpawnPoint.setTitle(i18n("world.info.spawn"));
String value;
// Valid after 1.21.9-pre1
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
value = (spawnTag.get("dimension") instanceof StringTag dimensionTag)
? Dimension.of(dimensionTag).formatPosition(posTag)
: Dimension.OVERWORLD.formatPosition(posTag);
}
// Valid before 1.21.9-pre1
else if (dataTag.get("SpawnX") instanceof IntTag intX
value = Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag
? dimensionTag
: new StringTag("SpawnDimension", "minecraft:overworld"))
.formatPosition(posTag);
} else if (dataTag.get("SpawnX") instanceof IntTag intX
&& dataTag.get("SpawnY") instanceof IntTag intY
&& dataTag.get("SpawnZ") instanceof IntTag intZ) {
value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
@@ -243,51 +254,34 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
{
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
generateFeaturesButton.setDisable(isReadOnly);
// Valid before (1.16)20w20a
if (dataTag.get("MapFeatures") instanceof ByteTag generateFeaturesTag) {
bindTagAndToggleButton(generateFeaturesTag, generateFeaturesButton);
}
// Valid after (1.16)20w20a
else if (world.getNormalizedWorldGenSettingsData() != null) {
CompoundTag unifiedWorldGenSettingsDataTag = world.getNormalizedWorldGenSettingsData();
Tag tag = unifiedWorldGenSettingsDataTag.get("generate_features"); // Valid between (1.16)20w20a and 26.1-snapshot-6
if (tag == null)
tag = unifiedWorldGenSettingsDataTag.get("generate_structures"); // Valid after 26.1-snapshot-6
bindTagAndToggleButton(tag, generateFeaturesButton);
// generate_features was valid after 20w20a and MapFeatures was before that
if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) {
bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton);
} else {
generateFeaturesButton.setDisable(true);
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
}
}
var difficultyButton = new LineSelectButton<Difficulty>();
{
difficultyButton.setTitle(i18n("world.info.difficulty"));
difficultyButton.setDisable(isReadOnly);
difficultyButton.setDisable(worldManagePage.isReadOnly());
difficultyButton.setItems(Difficulty.items);
Difficulty difficulty;
// Valid before 26.1-snapshot-6
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag
&& (difficulty = Difficulty.of(difficultyTag.getValue())) != null) {
difficultyButton.setValue(difficulty);
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
difficultyTag.setValue((byte) newValue.ordinal());
saveWorldData();
}
});
}
// Valid after 26.1-snapshot-6
else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag
&& difficultySettingTag.get("difficulty") instanceof StringTag difficultyTag
&& (difficulty = Difficulty.of(difficultyTag.getValue())) != null) {
difficultyButton.setValue(difficulty);
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
difficultyTag.setValue(newValue.getTagStringValue());
saveWorldData();
}
});
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
if (difficulty != null) {
difficultyButton.setValue(difficulty);
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
difficultyTag.setValue((byte) newValue.ordinal());
saveLevelDat();
}
});
} else {
difficultyButton.setDisable(true);
}
} else {
difficultyButton.setDisable(true);
}
@@ -297,17 +291,8 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
{
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
difficultyLockPane.setDisable(isReadOnly);
// Valid before 26.1-snapshot-6
if (dataTag.get("DifficultyLocked") instanceof ByteTag difficultyLockedTag) {
bindTagAndToggleButton(difficultyLockedTag, difficultyLockPane);
}
// Valid after 26.1-snapshot-6
else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag
&& difficultySettingTag.get("locked") instanceof ByteTag lockedTag) {
bindTagAndToggleButton(lockedTag, difficultyLockPane);
} else {
difficultyLockPane.setDisable(true);
}
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
}
worldInfo.getContent().setAll(
@@ -317,7 +302,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
}
if (playerTag != null) {
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
ComponentList playerInfo = new ComponentList();
var locationPane = new LineTextPane();
@@ -364,14 +349,8 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
playerGameTypePane.setItems(GameType.items);
// Valid before 26.1-snapshot-6
Tag rawHardcoreTag = dataTag.get("hardcore");
// Valid after 26.1-snapshot-6
if (rawHardcoreTag == null && dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingsTag) {
rawHardcoreTag = difficultySettingsTag.get("hardcore");
}
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag && rawHardcoreTag instanceof ByteTag hardcoreTag) {
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag
&& dataTag.get("hardcore") instanceof ByteTag hardcoreTag) {
boolean isHardcore = hardcoreTag.getValue() == 1;
GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore);
if (gameType != null) {
@@ -385,7 +364,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
playerGameTypeTag.setValue(newValue.ordinal());
hardcoreTag.setValue((byte) 0);
}
saveWorldData();
saveLevelDat();
}
});
} else {
@@ -455,10 +434,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> {
try {
byteTag.setValue((byte) (newValue ? 1 : 0));
saveWorldData();
saveLevelDat();
} catch (Exception e) {
toggleButton.setSelected(oldValue);
LOG.warning("Exception happened when saving world info", e);
LOG.warning("Exception happened when saving level.dat", e);
}
});
} else {
@@ -478,11 +457,11 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
Integer integer = Lang.toIntOrNull(newValue);
if (integer != null) {
intTag.setValue(integer);
saveWorldData();
saveLevelDat();
}
} catch (Exception e) {
jfxTextField.setText(oldValue);
LOG.warning("Exception happened when saving world data", e);
LOG.warning("Exception happened when saving level.dat", e);
}
}
});
@@ -499,11 +478,11 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
Float floatValue = Lang.toFloatOrNull(newValue);
if (floatValue != null) {
floatTag.setValue(floatValue);
saveWorldData();
saveLevelDat();
}
} catch (Exception e) {
jfxTextField.setText(oldValue);
LOG.warning("Exception happened when saving world data", e);
LOG.warning("Exception happened when saving level.dat", e);
}
}
});
@@ -511,27 +490,30 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true));
}
private void saveWorldData() {
LOG.info("Saving data of world " + world.getWorldName());
private void saveLevelDat() {
LOG.info("Saving level.dat of world " + world.getWorldName());
try {
this.world.writeWorldData();
this.world.writeLevelDat(levelDat);
} catch (IOException e) {
LOG.warning("Failed to save world data", e);
LOG.warning("Failed to save level.dat of world " + world.getWorldName(), e);
}
}
@Override
public void refresh() {
setFailedReason(null);
try {
this.isReadOnly = worldManagePage.isReadOnly();
this.levelData = world.getLevelData();
this.playerData = world.getPlayerData();
updateControls();
} catch (Exception e) {
LOG.warning("Failed to refresh world info", e);
setFailedReason(i18n("world.info.failed"));
}
this.isReadOnly = worldManagePage.isReadOnly();
this.setLoading(true);
Task.supplyAsync(this::loadWorldInfo)
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
this.levelDat = result;
updateControls();
setLoading(false);
} else {
LOG.warning("Failed to load level.dat", exception);
setFailedReason(i18n("world.info.failed"));
}
})).start();
}
private record Dimension(String name) {
@@ -612,15 +594,6 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
return (d >= 0 && d < items.size()) ? items.get(d) : null;
}
static Difficulty of(String name) {
for (Difficulty d : items) if (d.name().toLowerCase(Locale.ROOT).equals(name)) return d;
return null;
}
String getTagStringValue() {
return this.name().toLowerCase(Locale.ROOT);
}
@Override
public String toString() {
return i18n("world.info.difficulty." + name().toLowerCase(Locale.ROOT));
@@ -686,7 +659,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
Files.deleteIfExists(output);
iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png"));
} catch (IOException e) {
LOG.warning("Failed to delete world icon ", e);
LOG.warning("Failed to delete world icon " + e.getMessage());
}
}
}
@@ -30,6 +30,7 @@ import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -242,7 +243,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
private final WorldListPage page;
private final RipplerContainer graphic;
private final ImageContainer imageView;
private final ImageView imageView;
private final Tooltip leftTooltip;
private final TwoLineListItem content;
private final JFXButton btnLaunch;
@@ -261,8 +262,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
this.imageView = new ImageContainer(32);
this.imageView = new ImageView();
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
}
{
@@ -276,10 +278,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH);
btnLaunch = new JFXButton();
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
right.getChildren().add(btnLaunch);
btnLaunch.getStyleClass().add("toggle-icon4");
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
btnLaunch.setOnAction(event -> {
World world = getItem();
@@ -287,8 +291,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
page.launch(world);
});
JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT);
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
btnMore.setOnAction(event -> {
World world = getItem();
if (world != null)
@@ -382,7 +388,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
exportMenuItem.setDisable(worldLocked);
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> page.delete(world), popup);
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
deleteMenuItem.setDisable(worldLocked);
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
@@ -78,9 +78,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
updateSessionLockChannel();
try {
this.world.reloadWorldData();
this.world.reloadLevelDat();
} catch (IOException e) {
LOG.warning("Can not load world data of world: " + this.world.getFile(), e);
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
}
@@ -106,9 +106,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
public void refresh() {
updateSessionLockChannel();
try {
world.reloadWorldData();
world.reloadLevelDat();
} catch (IOException e) {
LOG.warning("Can not load world data of world: " + world.getFile(), e);
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
closePageForLoadingFail();
return;
}
@@ -273,7 +273,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup),
new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup)
);
@@ -0,0 +1,35 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.wizard;
public enum WizardNavigationResult {
PROCEED {
@Override
public boolean getDeferredComputation() {
return true;
}
},
DENY {
@Override
public boolean getDeferredComputation() {
return false;
}
};
public abstract boolean getDeferredComputation();
}
@@ -37,7 +37,6 @@ import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -175,25 +174,15 @@ public final class UpdateHandler {
startJava(updateTo, "--apply-to", self.toString());
}
public static void startJava(Path jar, String... appArgs) throws IOException {
private static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaRuntime.getDefault().getBinary().toString());
try {
for (String inputArgument : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
if (inputArgument.startsWith("-D") || inputArgument.startsWith("-X")) {
commandline.add(inputArgument);
}
}
} catch (Throwable ignored) {
// ManagementFactory not available
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
if (entry.getKey() instanceof String key && key.startsWith("hmcl.")) {
commandline.add("-D" + key + "=" + entry.getValue());
}
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
Object key = entry.getKey();
if (key instanceof String && ((String) key).startsWith("hmcl.")) {
commandline.add("-D" + key + "=" + entry.getValue());
}
}
commandline.add("-jar");
commandline.add(jar.toAbsolutePath().toString());
commandline.addAll(Arrays.asList(appArgs));
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff. Show more