Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48a3c2109 | ||
|
|
a0f9ba1976 | ||
|
|
b8df46ef87 | ||
|
|
0c04a013c8 | ||
|
|
3cf0acc505 | ||
|
|
3be5ea409b | ||
|
|
42f03411de | ||
|
|
941e7cd858 | ||
|
|
61c1b041cd | ||
|
|
7a48be5364 |
No files matched your search
@@ -9,15 +9,15 @@ body:
|
||||
value: |
|
||||
提交前请确认:
|
||||
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**。如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
* 你的启动器版本是**最新的预览版本**。你可以从 [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) 或 [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main) 下载最新预览版本。
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
|
||||
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
|
||||
Before submitting, please confirm:
|
||||
|
||||
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* Your launcher is the **latest nightly build**. You can download the latest nightly build from [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) or [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main).
|
||||
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: textarea
|
||||
@@ -38,7 +38,7 @@ body:
|
||||
attributes:
|
||||
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
||||
description: |
|
||||
如果你的启动器崩溃了,请将崩溃报告填入 (或将文件拖入) 下方。
|
||||
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||
|
||||
|
||||
@@ -1,357 +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.base.NodeConverter;
|
||||
import com.jfoenix.skins.JFXComboBoxListViewSkin;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXComboBox is the material design implementation of a combobox.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXComboBox<T> extends ComboBox<T> {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXComboBox() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXComboBox(ObservableList<T> items) {
|
||||
super(items);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.setCellFactory(listView -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateDisplayText(this, item, empty);
|
||||
}
|
||||
});
|
||||
|
||||
// had to refactor the code out of the skin class to allow
|
||||
// customization of the button cell
|
||||
this.setButtonCell(new ListCell<T>() {
|
||||
{
|
||||
// fixed clearing the combo box value is causing
|
||||
// java prompt text to be shown because the button cell is not updated
|
||||
JFXComboBox.this.valueProperty().addListener(observable -> {
|
||||
if (JFXComboBox.this.getValue() == null) {
|
||||
updateItem(null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
updateDisplayText(this, item, empty);
|
||||
this.setVisible(item != null || !empty);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
useJFXContextMenu(editorProperty().get());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXComboBoxListViewSkin<T>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-combo-box'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-combo-box";
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Node Converter Property *
|
||||
* *
|
||||
**************************************************************************/
|
||||
/**
|
||||
* Converts the user-typed input (when the ComboBox is
|
||||
* {@link #editableProperty() editable}) to an object of type T, such that
|
||||
* the input may be retrieved via the {@link #valueProperty() value} property.
|
||||
*/
|
||||
public ObjectProperty<NodeConverter<T>> nodeConverterProperty() {
|
||||
return nodeConverter;
|
||||
}
|
||||
|
||||
private ObjectProperty<NodeConverter<T>> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter",
|
||||
JFXComboBox.<T>defaultNodeConverter());
|
||||
|
||||
public final void setNodeConverter(NodeConverter<T> value) {
|
||||
nodeConverterProperty().set(value);
|
||||
}
|
||||
|
||||
public final NodeConverter<T> getNodeConverter() {
|
||||
return nodeConverterProperty().get();
|
||||
}
|
||||
|
||||
private static <T> NodeConverter<T> defaultNodeConverter() {
|
||||
return new NodeConverter<T>() {
|
||||
@Override
|
||||
public Node toNode(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
StackPane selectedValueContainer = new StackPane();
|
||||
selectedValueContainer.getStyleClass().add("combo-box-selected-value-container");
|
||||
selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
Label selectedValueLabel = object instanceof Label ? new Label(((Label) object).getText()) : new Label(
|
||||
object.toString());
|
||||
selectedValueLabel.setTextFill(Color.BLACK);
|
||||
selectedValueContainer.getChildren().add(selectedValueLabel);
|
||||
StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT);
|
||||
StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5));
|
||||
return selectedValueContainer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T fromNode(Node node) {
|
||||
return (T) node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
if (object instanceof Label) {
|
||||
return ((Label) object).getText();
|
||||
}
|
||||
return object.toString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
|
||||
if (empty) {
|
||||
// create empty cell
|
||||
if (cell == null) {
|
||||
return true;
|
||||
}
|
||||
cell.setGraphic(null);
|
||||
cell.setText(null);
|
||||
return true;
|
||||
} else if (item instanceof Node) {
|
||||
Node currentNode = cell.getGraphic();
|
||||
Node newNode = (Node) item;
|
||||
// create a node from the selected node of the listview
|
||||
// using JFXComboBox {@link #nodeConverterProperty() NodeConverter})
|
||||
NodeConverter<T> nc = this.getNodeConverter();
|
||||
Node node = nc == null ? null : nc.toNode(item);
|
||||
if (currentNode == null || !currentNode.equals(newNode)) {
|
||||
cell.setText(null);
|
||||
cell.setGraphic(node == null ? newNode : node);
|
||||
}
|
||||
return node == null;
|
||||
} else {
|
||||
// run item through StringConverter if it isn't null
|
||||
StringConverter<T> c = this.getConverter();
|
||||
String s = item == null ? this.getPromptText() : (c == null ? item.toString() : c.toString(item));
|
||||
cell.setText(s);
|
||||
cell.setGraphic(null);
|
||||
return s == null || s.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT,
|
||||
JFXComboBox.this,
|
||||
"lableFloat",
|
||||
false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR,
|
||||
JFXComboBox.this,
|
||||
"unFocusColor",
|
||||
Color.rgb(77,
|
||||
77,
|
||||
77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR,
|
||||
JFXComboBox.this,
|
||||
"focusColor",
|
||||
Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXComboBox<?>, Paint> UNFOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
||||
"-jfx-unfocus-color",
|
||||
PaintConverter.getInstance(),
|
||||
Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXComboBox<?>, Paint> FOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
||||
"-jfx-focus-color",
|
||||
PaintConverter.getInstance(),
|
||||
Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXComboBox<?>, Boolean> LABEL_FLOAT = new CssMetaData<JFXComboBox<?>, Boolean>(
|
||||
"-jfx-label-float",
|
||||
BooleanConverter.getInstance(),
|
||||
false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
||||
Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
||||
Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(Control.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,297 +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.JFXPasswordFieldSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXPasswordField is the material design implementation of a password Field.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXPasswordField extends PasswordField {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXPasswordField() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXPasswordFieldSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
|
||||
}
|
||||
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-password-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-password-field";
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the password field in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the password value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the password value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXPasswordField.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXPasswordField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXPasswordField.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXPasswordField.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXPasswordField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXPasswordField, Paint> FOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXPasswordField, Boolean> LABEL_FLOAT = new CssMetaData<JFXPasswordField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXPasswordField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXPasswordField, Boolean>("-fx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextField.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,10 +24,8 @@ import javafx.application.Platform;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.PopupControl;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Pane;
|
||||
@@ -43,12 +41,7 @@ import javafx.stage.Window;
|
||||
public class JFXPopup extends PopupControl {
|
||||
|
||||
public enum PopupHPosition {
|
||||
RIGHT,
|
||||
LEFT;
|
||||
|
||||
public PopupHPosition getOpposite() {
|
||||
return (this == RIGHT) ? LEFT : RIGHT;
|
||||
}
|
||||
RIGHT, LEFT
|
||||
}
|
||||
|
||||
public enum PopupVPosition {
|
||||
@@ -128,21 +121,18 @@ public class JFXPopup extends PopupControl {
|
||||
/// @param initOffsetY on the y-axis
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
if (!isShowing()) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null || scene.getWindow() == null) {
|
||||
if (node.getScene() == null || node.getScene().getWindow() == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = scene.getWindow();
|
||||
Window parent = node.getScene().getWindow();
|
||||
final Point2D origin = node.localToScene(0, 0);
|
||||
|
||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
||||
|
||||
this.show(node,
|
||||
parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0),
|
||||
parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0)
|
||||
);
|
||||
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
|
||||
final double anchorX = parent.getX() + origin.getX()
|
||||
+ node.getScene().getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
||||
final double anchorY = parent.getY() + origin.getY()
|
||||
+ node.getScene()
|
||||
.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
||||
this.show(parent, anchorX, anchorY);
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,297 +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.JFXTextAreaSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXTextArea is the material design implementation of a text area.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXTextArea extends TextArea {
|
||||
/**
|
||||
* Initialize the style class to 'jfx-text-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-area";
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextArea() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextArea(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXTextAreaSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextAreaSkinAndroid\";");
|
||||
}
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the text area in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the text value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the text value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextArea.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the text area is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the text area is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextArea.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextArea.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXTextArea, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.rgb(77, 77, 77)) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextArea, Paint> FOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#4059A9")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextArea, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextArea, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXTextArea, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextArea, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextArea.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -1,299 +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.JFXTextFieldSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXTextField is the material design implementation of a text Field.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXTextField extends TextField {
|
||||
/**
|
||||
* Initialize the style class to 'jfx-text-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-field";
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextField() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextField(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXTextFieldSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextFieldSkinAndroid\";");
|
||||
}
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the text field in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the text value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the text value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextField.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextField.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextField.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXTextField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextField, Paint> FOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextField, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXTextField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextField, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextField.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -50,12 +50,10 @@ public final class EntryPoint {
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
||||
if (!isInsideMacAppBundle())
|
||||
initIcon();
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
@@ -194,6 +192,16 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDir = System.getProperty("user.dir", "");
|
||||
if (currentDir.contains("!")) {
|
||||
LOG.error("The current working path contains an exclamation mark: " + currentDir);
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
|
||||
@@ -253,7 +253,7 @@ public final class Launcher extends Application {
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
if (showAlert(AlertType.ERROR,
|
||||
|
||||
@@ -46,7 +46,6 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
@@ -322,8 +321,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC))
|
||||
return VersionIconType.LEGACY_FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
@@ -422,7 +419,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setProxyOption(getProxyOption())
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
@@ -442,6 +438,17 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
|
||||
}
|
||||
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxyType(config().getProxyType());
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
builder.setProxyPort(config().getProxyPort());
|
||||
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
}
|
||||
|
||||
Path json = getModpackConfiguration(version);
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
@@ -551,39 +558,4 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return minimum;
|
||||
}
|
||||
}
|
||||
|
||||
public static ProxyOption getProxyOption() {
|
||||
if (!config().hasProxy() || config().getProxyType() == null) {
|
||||
return ProxyOption.Default.INSTANCE;
|
||||
}
|
||||
|
||||
return switch (config().getProxyType()) {
|
||||
case DIRECT -> ProxyOption.Direct.INSTANCE;
|
||||
case HTTP, SOCKS -> {
|
||||
String proxyHost = config().getProxyHost();
|
||||
int proxyPort = config().getProxyPort();
|
||||
|
||||
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
|
||||
yield ProxyOption.Default.INSTANCE;
|
||||
}
|
||||
|
||||
String proxyUser = config().getProxyUser();
|
||||
String proxyPass = config().getProxyPass();
|
||||
|
||||
if (StringUtils.isBlank(proxyUser)) {
|
||||
proxyUser = null;
|
||||
proxyPass = null;
|
||||
} else if (proxyPass == null) {
|
||||
proxyPass = "";
|
||||
}
|
||||
|
||||
if (config().getProxyType() == Proxy.Type.HTTP) {
|
||||
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
} else {
|
||||
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
}
|
||||
}
|
||||
default -> ProxyOption.Default.INSTANCE;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -325,13 +325,13 @@ public final class LauncherHelper {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||
(result, handler) -> {
|
||||
(result, resolve, reject) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
@@ -389,7 +389,7 @@ public final class LauncherHelper {
|
||||
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
|
||||
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
|
||||
Controllers.dialog(
|
||||
i18n("launch.failed.java_version_too_low"),
|
||||
i18n("message.error"),
|
||||
@@ -634,7 +634,7 @@ public final class LauncherHelper {
|
||||
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
||||
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile, true)) {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
|
||||
|
||||
@@ -30,9 +30,7 @@ import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -59,7 +57,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,7 +66,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, String name) {
|
||||
@@ -76,7 +74,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, String name) {
|
||||
@@ -96,7 +94,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
|
||||
@@ -171,7 +169,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>();
|
||||
update.put("provider", "mojang");
|
||||
update.put("component", gameJavaVersion.component());
|
||||
update.put("component", gameJavaVersion.getComponent());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles.getFiles().forEach((path, file) -> {
|
||||
|
||||
@@ -36,9 +36,7 @@ public enum VersionIconType {
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
CLEANROOM("/assets/img/cleanroom.png"),
|
||||
LEGACY_FABRIC("/assets/img/legacyfabric.png")
|
||||
;
|
||||
CLEANROOM("/assets/img/cleanroom.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -25,10 +24,7 @@ import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.glavo.monetfx.Contrast;
|
||||
@@ -36,23 +32,13 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Themes {
|
||||
@@ -79,7 +65,7 @@ public final class Themes {
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
yield FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT;
|
||||
} else {
|
||||
yield getDefaultBrightness();
|
||||
yield Brightness.DEFAULT;
|
||||
}
|
||||
}
|
||||
case "dark" -> Brightness.DARK;
|
||||
@@ -111,66 +97,6 @@ public final class Themes {
|
||||
theme.addListener(listener);
|
||||
}
|
||||
|
||||
private static Brightness defaultBrightness;
|
||||
|
||||
private static Brightness getDefaultBrightness() {
|
||||
if (defaultBrightness != null)
|
||||
return defaultBrightness;
|
||||
|
||||
LOG.info("Detecting system theme brightness");
|
||||
Brightness brightness = Brightness.DEFAULT;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg != null) {
|
||||
Object appsUseLightTheme = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "AppsUseLightTheme");
|
||||
if (appsUseLightTheme instanceof Integer value) {
|
||||
brightness = value == 0 ? Brightness.DARK : Brightness.LIGHT;
|
||||
}
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
try {
|
||||
String result = SystemUtils.run("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle").trim();
|
||||
brightness = "Dark".equalsIgnoreCase(result) ? Brightness.DARK : Brightness.LIGHT;
|
||||
} catch (Exception e) {
|
||||
// If the key does not exist, it means Light mode is used
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
Path dbusSend = SystemUtils.which("dbus-send");
|
||||
if (dbusSend != null) {
|
||||
try {
|
||||
String[] result = SystemUtils.run(List.of(
|
||||
FileUtils.getAbsolutePath(dbusSend),
|
||||
"--session",
|
||||
"--print-reply=literal",
|
||||
"--reply-timeout=1000",
|
||||
"--dest=org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings.Read",
|
||||
"string:org.freedesktop.appearance",
|
||||
"string:color-scheme"
|
||||
), Duration.ofSeconds(2)).trim().split(" ");
|
||||
|
||||
if (result.length > 0) {
|
||||
String value = result[result.length - 1];
|
||||
// 1: prefer dark
|
||||
// 2: prefer light
|
||||
if ("1".equals(value)) {
|
||||
brightness = Brightness.DARK;
|
||||
} else if ("2".equals(value)) {
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to get system theme from D-Bus", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.info("Detected system theme brightness: " + brightness);
|
||||
|
||||
return defaultBrightness = brightness;
|
||||
}
|
||||
|
||||
public static ObjectExpression<Theme> themeProperty() {
|
||||
return theme;
|
||||
}
|
||||
@@ -203,39 +129,6 @@ public final class Themes {
|
||||
return darkMode;
|
||||
}
|
||||
|
||||
public static void applyNativeDarkMode(Stage stage) {
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), darkMode -> {
|
||||
if (stage.isShowing()) {
|
||||
WindowsNativeUtils.getWindowHandle(stage).ifPresent(handle -> {
|
||||
if (handle == WinTypes.HANDLE.INVALID_VALUE)
|
||||
return;
|
||||
|
||||
Dwmapi.INSTANCE.DwmSetWindowAttribute(
|
||||
new WinTypes.HANDLE(Pointer.createConstant(handle)),
|
||||
WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)),
|
||||
WinTypes.BOOL.SIZE
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
|
||||
if (stage.isShowing()) {
|
||||
listener.changed(null, false, Themes.darkModeProperty().get());
|
||||
} else {
|
||||
stage.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(WindowEvent event) {
|
||||
stage.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
|
||||
listener.changed(null, false, Themes.darkModeProperty().get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Themes() {
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -27,10 +26,7 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
@@ -43,23 +39,20 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
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.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -68,13 +61,9 @@ 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.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
@@ -99,14 +88,12 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.net.*;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.*;
|
||||
@@ -115,8 +102,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
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 FXUtils {
|
||||
private FXUtils() {
|
||||
@@ -312,14 +299,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
return limitingSize(node, 30, 20);
|
||||
}
|
||||
|
||||
public static Node wrap(SVG svg) {
|
||||
return wrap(svg.createIcon(20));
|
||||
}
|
||||
|
||||
private static class ListenerPair<T> {
|
||||
private final ObservableValue<T> value;
|
||||
private final ChangeListener<? super T> listener;
|
||||
@@ -433,20 +412,6 @@ public final class FXUtils {
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void limitCellWidth(ListView<?> listView, ListCell<?> cell) {
|
||||
ReadOnlyDoubleProperty widthProperty;
|
||||
|
||||
if (listView.lookup(".clipped-container") instanceof Region clippedContainer) {
|
||||
widthProperty = clippedContainer.widthProperty();
|
||||
} else {
|
||||
widthProperty = listView.widthProperty();
|
||||
}
|
||||
|
||||
cell.maxWidthProperty().bind(widthProperty);
|
||||
cell.prefWidthProperty().bind(widthProperty);
|
||||
cell.minWidthProperty().bind(widthProperty);
|
||||
}
|
||||
|
||||
public static void smoothScrolling(ScrollPane scrollPane) {
|
||||
if (AnimationUtils.isAnimationEnabled())
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
@@ -1299,8 +1264,8 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel() {
|
||||
Label label = new Label();
|
||||
public static Label newSafeTruncatedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
showTooltipWhenTruncated(label);
|
||||
return label;
|
||||
@@ -1432,15 +1397,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void onSecondaryButtonClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <N extends Parent> N prepareNode(N node) {
|
||||
Scene dummyScene = new Scene(node);
|
||||
StyleSheets.init(dummyScene);
|
||||
@@ -1636,39 +1592,4 @@ public final class FXUtils {
|
||||
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
|
||||
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
|
||||
}
|
||||
|
||||
public static void useJFXContextMenu(TextInputControl control) {
|
||||
control.setContextMenu(null);
|
||||
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.setAutoHide(true);
|
||||
|
||||
control.setOnContextMenuRequested(e -> {
|
||||
boolean hasNoSelection = control.getSelectedText().isEmpty();
|
||||
|
||||
IconedMenuItem undo = new IconedMenuItem(SVG.UNDO, i18n("menu.undo"), control::undo, popup);
|
||||
IconedMenuItem redo = new IconedMenuItem(SVG.REDO, i18n("menu.redo"), control::redo, popup);
|
||||
IconedMenuItem cut = new IconedMenuItem(SVG.CONTENT_CUT, i18n("menu.cut"), control::cut, popup);
|
||||
IconedMenuItem copy = new IconedMenuItem(SVG.CONTENT_COPY, i18n("menu.copy"), control::copy, popup);
|
||||
IconedMenuItem paste = new IconedMenuItem(SVG.CONTENT_PASTE, i18n("menu.paste"), control::paste, popup);
|
||||
IconedMenuItem delete = new IconedMenuItem(SVG.DELETE, i18n("menu.deleteselection"), () -> control.replaceSelection(""), popup);
|
||||
IconedMenuItem selectall = new IconedMenuItem(SVG.SELECT_ALL, i18n("menu.selectall"), control::selectAll, popup);
|
||||
|
||||
menu.getContent().setAll(undo, redo, new MenuSeparator(), cut, copy, paste, delete, new MenuSeparator(), selectall);
|
||||
|
||||
undo.setDisable(!control.isUndoable());
|
||||
redo.setDisable(!control.isRedoable());
|
||||
cut.setDisable(hasNoSelection);
|
||||
delete.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection);
|
||||
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
|
||||
selectall.setDisable(control.getText() == null || control.getText().isEmpty());
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(control, popup);
|
||||
popup.show(control, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - control.getHeight());
|
||||
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -91,8 +90,6 @@ public class GameCrashWindow extends Stage {
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
|
||||
Themes.applyNativeDarkMode(this);
|
||||
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
|
||||
@@ -92,18 +92,37 @@ public class InstallerItem extends Control {
|
||||
this.id = id;
|
||||
this.style = style;
|
||||
|
||||
iconType = switch (id) {
|
||||
case "game" -> VersionIconType.GRASS;
|
||||
case "fabric", "fabric-api" -> VersionIconType.FABRIC;
|
||||
case "legacyfabric", "legacyfabric-api" -> VersionIconType.LEGACY_FABRIC;
|
||||
case "forge" -> VersionIconType.FORGE;
|
||||
case "cleanroom" -> VersionIconType.CLEANROOM;
|
||||
case "liteloader" -> VersionIconType.CHICKEN;
|
||||
case "optifine" -> VersionIconType.OPTIFINE;
|
||||
case "quilt", "quilt-api" -> VersionIconType.QUILT;
|
||||
case "neoforge" -> VersionIconType.NEO_FORGE;
|
||||
default -> null;
|
||||
};
|
||||
switch (id) {
|
||||
case "game":
|
||||
iconType = VersionIconType.GRASS;
|
||||
break;
|
||||
case "fabric":
|
||||
case "fabric-api":
|
||||
iconType = VersionIconType.FABRIC;
|
||||
break;
|
||||
case "forge":
|
||||
iconType = VersionIconType.FORGE;
|
||||
break;
|
||||
case "cleanroom":
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
break;
|
||||
case "liteloader":
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
break;
|
||||
case "optifine":
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
break;
|
||||
case "quilt":
|
||||
case "quilt-api":
|
||||
iconType = VersionIconType.QUILT;
|
||||
break;
|
||||
case "neoforge":
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
break;
|
||||
default:
|
||||
iconType = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public String getLibraryId() {
|
||||
@@ -182,8 +201,6 @@ public class InstallerItem extends Control {
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
|
||||
InstallerItem legacyfabric = new InstallerItem(LEGACY_FABRIC, style);
|
||||
InstallerItem legacyfabricApi = new InstallerItem(LEGACY_FABRIC_API, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
@@ -191,11 +208,11 @@ public class InstallerItem extends Control {
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom, liteLoader, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabricApi, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
@@ -229,7 +246,7 @@ public class InstallerItem extends Control {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
}
|
||||
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, legacyfabric, legacyfabricApi, cleanroom};
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, cleanroom};
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
@@ -246,9 +263,9 @@ public class InstallerItem extends Control {
|
||||
if (gameVersion == null) {
|
||||
this.libraries = all;
|
||||
} else if (gameVersion.equals("1.12.2")) {
|
||||
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, legacyfabric, legacyfabricApi, optiFine};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13.2") <= 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine, legacyfabric, legacyfabricApi};
|
||||
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, optiFine};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
public abstract class ListPage<T extends Node> extends ListPageBase<T> {
|
||||
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
|
||||
|
||||
public abstract void add();
|
||||
|
||||
public void refresh() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ListPageSkin(this);
|
||||
}
|
||||
|
||||
public boolean isRefreshable() {
|
||||
return refreshable.get();
|
||||
}
|
||||
|
||||
public BooleanProperty refreshableProperty() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public void setRefreshable(boolean refreshable) {
|
||||
this.refreshable.set(refreshable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
|
||||
public ListPageSkin(ListPage<?> skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
Pane placeholder = new Pane();
|
||||
VBox list = new VBox();
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
{
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
list.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
list.setSpacing(10);
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getChildren().setAll(list, placeholder);
|
||||
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
{
|
||||
vBox.getStyleClass().add("card-list");
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
vBox.setPickOnBounds(false);
|
||||
|
||||
JFXButton btnAdd = FXUtils.newRaisedButton("");
|
||||
FXUtils.setLimitWidth(btnAdd, 40);
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.setGraphic(SVG.ADD.createIcon());
|
||||
btnAdd.setOnAction(e -> skinnable.add());
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
FXUtils.setLimitWidth(btnRefresh, 40);
|
||||
FXUtils.setLimitHeight(btnRefresh, 40);
|
||||
btnRefresh.getStyleClass().add("jfx-button-raised-round");
|
||||
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
|
||||
btnRefresh.setOnAction(e -> skinnable.refresh());
|
||||
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
|
||||
refreshable -> {
|
||||
if (refreshable) {
|
||||
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
|
||||
vBox.getChildren().setAll(btnRefresh, btnAdd);
|
||||
} else {
|
||||
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keep a blank space to prevent buttons from blocking up mod items.
|
||||
BorderPane group = new BorderPane();
|
||||
group.setPickOnBounds(false);
|
||||
group.setBottom(vBox);
|
||||
placeholder.minHeightProperty().bind(vBox.heightProperty());
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, group);
|
||||
}
|
||||
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.setContent(contentPane);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -28,6 +29,7 @@ import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
@@ -36,6 +38,7 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
@@ -45,6 +48,9 @@ import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -81,13 +87,41 @@ public final class LogWindow extends Stage {
|
||||
private final LogWindowImpl impl;
|
||||
private final ManagedProcess gameProcess;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Object windowsDarkModeListenerHolder;
|
||||
|
||||
{
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
this.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(WindowEvent event) {
|
||||
LogWindow.this.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
|
||||
|
||||
windowsDarkModeListenerHolder = FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
if (LogWindow.this.isShowing()) {
|
||||
WindowsNativeUtils.getWindowHandle(LogWindow.this).ifPresent(handle -> {
|
||||
if (handle == WinTypes.HANDLE.INVALID_VALUE)
|
||||
return;
|
||||
|
||||
Dwmapi.INSTANCE.DwmSetWindowAttribute(
|
||||
new WinTypes.HANDLE(Pointer.createConstant(handle)),
|
||||
WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)),
|
||||
WinTypes.BOOL.SIZE
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess) {
|
||||
this(gameProcess, new CircularArrayList<>());
|
||||
}
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
|
||||
Themes.applyNativeDarkMode(this);
|
||||
|
||||
this.logs = logs;
|
||||
this.impl = new LogWindowImpl();
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
|
||||
@@ -50,9 +50,7 @@ public enum SVG {
|
||||
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
|
||||
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 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"),
|
||||
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
|
||||
CONTENT_CUT("m12 14l-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22t-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12L7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6t1.175-2.825T6 2t2.825 1.175T10 6q0 .425-.075.85t-.275.8L20.6 18.6q.675.675.3 1.538T19.575 21q-.275 0-.537-.112t-.463-.313zm3-3l-2-2l5.575-5.575q.2-.2.463-.312T19.574 3q.95 0 1.313.875t-.313 1.55zM6 8q.825 0 1.413-.587T8 6t-.587-1.412T6 4t-1.412.588T4 6t.588 1.413T6 8m6 4.5q.2 0 .35-.15t.15-.35t-.15-.35t-.35-.15t-.35.15t-.15.35t.15.35t.35.15M6 20q.825 0 1.413-.587T8 18t-.587-1.412T6 16t-1.412.588T4 18t.588 1.413T6 20"),
|
||||
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v2q0 .425-.288.713T16 8H8q-.425 0-.712-.288T7 7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
@@ -104,7 +102,6 @@ public enum SVG {
|
||||
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
|
||||
PUBLIC("M12 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 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
|
||||
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
|
||||
REDO("M16.2 10H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H16q.425 0 .713.288T17 18t-.288.713T16 19H9.9q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-1.9-1.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.6 3.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.6 3.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"),
|
||||
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
@@ -123,9 +120,7 @@ public enum SVG {
|
||||
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
UNFOLD_MORE("M12 21 7.5 16.5l1.45-1.45L12 18.1l3.05-3.05 1.45 1.45L12 21ZM8.95 9.05 7.5 7.6 12 3.1l4.5 4.5-1.45 1.45L12 6 8.95 9.05Z"),
|
||||
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
|
||||
UNDO("M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
|
||||
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
|
||||
@@ -25,8 +25,6 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -67,7 +65,6 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
|
||||
ComponentList.setVgrow(listView, Priority.ALWAYS);
|
||||
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
root.getContent().add(listView);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -1,79 +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.JFXPopup;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class AccountListPopupMenu extends StackPane {
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY) {
|
||||
var menu = new AccountListPopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
}
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final BooleanBinding isEmpty = Bindings.isEmpty(Accounts.getAccounts());
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener listener;
|
||||
|
||||
public AccountListPopupMenu() {
|
||||
AdvancedListBox box = new AdvancedListBox();
|
||||
box.getStyleClass().add("no-padding");
|
||||
box.setPrefWidth(220);
|
||||
box.setPrefHeight(-1);
|
||||
box.setMaxHeight(260);
|
||||
|
||||
listener = o -> {
|
||||
box.clear();
|
||||
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
AccountAdvancedListItem item = new AccountAdvancedListItem(account);
|
||||
item.setOnAction(e -> {
|
||||
Accounts.setSelectedAccount(account);
|
||||
if (getScene().getWindow() instanceof JFXPopup popup)
|
||||
popup.hide();
|
||||
});
|
||||
box.add(item);
|
||||
}
|
||||
};
|
||||
listener.invalidated(null);
|
||||
Accounts.getAccounts().addListener(new WeakInvalidationListener(listener));
|
||||
|
||||
Label placeholder = new Label(i18n("account.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
getChildren().setAll(empty ? placeholder : box);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -118,7 +118,6 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
lblServerWarning.setStyle("-fx-text-fill: red;");
|
||||
GridPane.setColumnIndex(lblServerWarning, 0);
|
||||
GridPane.setRowIndex(lblServerWarning, 2);
|
||||
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
|
||||
GridPane.setColumnSpan(lblServerWarning, 2);
|
||||
|
||||
body.getChildren().setAll(
|
||||
|
||||
@@ -30,6 +30,7 @@ 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.versions.VersionPage;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -74,7 +75,7 @@ public class AdvancedListBox extends ScrollPane {
|
||||
item.setActionButtonVisible(false);
|
||||
item.setTitle(title);
|
||||
if (leftGraphic != null) {
|
||||
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
|
||||
item.setLeftGraphic(VersionPage.wrap(leftGraphic));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
public AdvancedListItemSkin(AdvancedListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -28,21 +33,88 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class ComponentList extends Control implements NoPaddingComponent {
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class ComponentList extends Control {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle", "");
|
||||
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
|
||||
private boolean hasSubtitle = false;
|
||||
public final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private Supplier<List<? extends Node>> lazyInitializer;
|
||||
|
||||
public ComponentList() {
|
||||
getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
this();
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth.get();
|
||||
}
|
||||
|
||||
public IntegerProperty depthProperty() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth.set(depth);
|
||||
}
|
||||
|
||||
public boolean isHasSubtitle() {
|
||||
return hasSubtitle;
|
||||
}
|
||||
|
||||
public void setHasSubtitle(boolean hasSubtitle) {
|
||||
this.hasSubtitle = hasSubtitle;
|
||||
}
|
||||
|
||||
public ObservableList<Node> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Orientation getContentBias() {
|
||||
return Orientation.HORIZONTAL;
|
||||
@@ -58,72 +130,49 @@ public class ComponentList extends Control implements NoPaddingComponent {
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final ObservableList<Node> list;
|
||||
private final ObjectBinding<Node> firstItem;
|
||||
private final ObjectBinding<Node> lastItem;
|
||||
|
||||
Skin(ComponentList control) {
|
||||
super(control);
|
||||
|
||||
list = MappedObservableList.create(control.getContent(), node -> {
|
||||
Pane wrapper;
|
||||
if (node instanceof ComponentSublist sublist) {
|
||||
sublist.getStyleClass().remove("options-list");
|
||||
sublist.getStyleClass().add("options-sublist");
|
||||
wrapper = new ComponentSublistWrapper(sublist);
|
||||
} else {
|
||||
wrapper = new StackPane(node);
|
||||
ComponentListCell cell = new ComponentListCell(node);
|
||||
cell.getStyleClass().add("options-list-item");
|
||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
||||
}
|
||||
|
||||
wrapper.getStyleClass().add("options-list-item");
|
||||
|
||||
if (node.getProperties().get("ComponentList.vgrow") instanceof Priority priority) {
|
||||
VBox.setVgrow(wrapper, priority);
|
||||
if (node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||
cell.getStyleClass().add("no-padding");
|
||||
}
|
||||
|
||||
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||
wrapper.getStyleClass().add("no-padding");
|
||||
}
|
||||
return wrapper;
|
||||
return cell;
|
||||
});
|
||||
|
||||
updateStyle();
|
||||
list.addListener((InvalidationListener) o -> updateStyle());
|
||||
firstItem = Bindings.valueAt(list, 0);
|
||||
firstItem.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
if (oldValue != null)
|
||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
||||
});
|
||||
if (!list.isEmpty())
|
||||
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
|
||||
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
|
||||
lastItem.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
if (oldValue != null)
|
||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||
});
|
||||
if (!list.isEmpty())
|
||||
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.setFillWidth(true);
|
||||
Bindings.bindContent(vbox.getChildren(), list);
|
||||
node = vbox;
|
||||
}
|
||||
|
||||
private Node prevFirstItem;
|
||||
private Node prevLastItem;
|
||||
|
||||
private void updateStyle() {
|
||||
Node firstItem;
|
||||
Node lastItem;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
firstItem = null;
|
||||
lastItem = null;
|
||||
} else {
|
||||
firstItem = list.get(0);
|
||||
lastItem = list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
if (firstItem != prevFirstItem) {
|
||||
if (prevFirstItem != null)
|
||||
prevFirstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
||||
if (firstItem != null)
|
||||
firstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
prevFirstItem = firstItem;
|
||||
}
|
||||
|
||||
if (lastItem != prevLastItem) {
|
||||
if (prevLastItem != null)
|
||||
prevLastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||
if (lastItem != null)
|
||||
lastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
prevLastItem = lastItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Node createComponentListTitle(String title) {
|
||||
@@ -140,8 +189,4 @@ public class ComponentList extends Control implements NoPaddingComponent {
|
||||
public static void setVgrow(Node node, Priority priority) {
|
||||
node.getProperties().put("ComponentList.vgrow", priority);
|
||||
}
|
||||
|
||||
public static void setNoPadding(Node node) {
|
||||
node.getProperties().put("ComponentList.noPadding", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
final class ComponentListCell extends StackPane {
|
||||
private final Node content;
|
||||
private Animation expandAnimation;
|
||||
private Rectangle clipRect;
|
||||
private final BooleanProperty expanded = new SimpleBooleanProperty(this, "expanded", false);
|
||||
|
||||
ComponentListCell(Node content) {
|
||||
this.content = content;
|
||||
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
private void updateClip(double newHeight) {
|
||||
if (clipRect != null)
|
||||
clipRect.setHeight(newHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
|
||||
if (clipRect == null)
|
||||
clipRect = new Rectangle(0, 0, getWidth(), getHeight());
|
||||
else {
|
||||
clipRect.setX(0);
|
||||
clipRect.setY(0);
|
||||
clipRect.setHeight(getHeight());
|
||||
clipRect.setWidth(getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayout() {
|
||||
if (content instanceof ComponentList list) {
|
||||
content.getStyleClass().remove("options-list");
|
||||
content.getStyleClass().add("options-sublist");
|
||||
|
||||
getStyleClass().add("no-padding");
|
||||
|
||||
VBox groupNode = new VBox();
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
expandIcon.setMouseTransparent(true);
|
||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
boolean overrideHeaderLeft = false;
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
||||
if (leftNode != null) {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
overrideHeaderLeft = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overrideHeaderLeft) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
label.getStyleClass().add("title-label");
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (list.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(list.subtitleProperty());
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(16);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
if (rightNode != null)
|
||||
header.getChildren().add(rightNode);
|
||||
}
|
||||
header.getChildren().add(expandIcon);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
groupNode.getChildren().add(headerRippler);
|
||||
|
||||
VBox container = new VBox();
|
||||
boolean hasPadding = !(list instanceof ComponentSublist subList) || subList.hasComponentPadding();
|
||||
if (hasPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(content);
|
||||
groupNode.getChildren().add(container);
|
||||
|
||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
event.consume();
|
||||
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
boolean expanded = !isExpanded();
|
||||
setExpanded(expanded);
|
||||
if (expanded) {
|
||||
list.doLazyInit();
|
||||
list.layout();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 4)) * (expanded ? 1 : -1);
|
||||
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(list.getWidth());
|
||||
double contentHeight = expanded ? newAnimatedHeight : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
double currentRotate = expandIcon.getRotate();
|
||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||
|
||||
expandAnimation = new Timeline(
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||
);
|
||||
|
||||
if (!expanded) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
}
|
||||
|
||||
expandAnimation.play();
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
expandIcon.setRotate(targetRotate);
|
||||
|
||||
if (!expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else {
|
||||
getStyleClass().remove("no-padding");
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return expanded.get();
|
||||
}
|
||||
|
||||
public BooleanProperty expandedProperty() {
|
||||
return expanded;
|
||||
}
|
||||
|
||||
public void setExpanded(boolean expanded) {
|
||||
this.expanded.set(expanded);
|
||||
}
|
||||
}
|
||||
@@ -17,100 +17,57 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class ComponentSublist extends ComponentList {
|
||||
|
||||
Supplier<List<? extends Node>> lazyInitializer;
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
|
||||
|
||||
public ComponentSublist() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ComponentSublist(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null)
|
||||
subtitle = new SimpleStringProperty(this, "subtitle", "");
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
private boolean hasSubtitle = false;
|
||||
|
||||
public boolean isHasSubtitle() {
|
||||
return hasSubtitle;
|
||||
}
|
||||
|
||||
public void setHasSubtitle(boolean hasSubtitle) {
|
||||
this.hasSubtitle = hasSubtitle;
|
||||
}
|
||||
|
||||
private Node headerLeft;
|
||||
|
||||
public Node getHeaderLeft() {
|
||||
return headerLeft.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerLeftProperty() {
|
||||
return headerLeft;
|
||||
}
|
||||
|
||||
public void setHeaderLeft(Node headerLeft) {
|
||||
this.headerLeft = headerLeft;
|
||||
this.headerLeft.set(headerLeft);
|
||||
}
|
||||
|
||||
private Node headerRight;
|
||||
|
||||
public Node getHeaderRight() {
|
||||
return headerRight.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerRightProperty() {
|
||||
return headerRight;
|
||||
}
|
||||
|
||||
public void setHeaderRight(Node headerRight) {
|
||||
this.headerRight = headerRight;
|
||||
this.headerRight.set(headerRight);
|
||||
}
|
||||
|
||||
private boolean componentPadding = true;
|
||||
|
||||
public boolean hasComponentPadding() {
|
||||
return componentPadding.get();
|
||||
}
|
||||
|
||||
public BooleanProperty componentPaddingProperty() {
|
||||
return componentPadding;
|
||||
}
|
||||
|
||||
public void setComponentPadding(boolean componentPadding) {
|
||||
this.componentPadding = componentPadding;
|
||||
this.componentPadding.set(componentPadding);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +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.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// @author Glavo
|
||||
final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
private VBox container;
|
||||
|
||||
private Animation expandAnimation;
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||
boolean noPadding = !sublist.hasComponentPadding();
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
expandIcon.setMouseTransparent(true);
|
||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Node leftNode = sublist.getHeaderLeft();
|
||||
if (leftNode == null) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(sublist.titleProperty());
|
||||
label.getStyleClass().add("title-label");
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (sublist.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(sublist.subtitleProperty());
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
}
|
||||
} else {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
}
|
||||
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(16);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||
Node rightNode = sublist.getHeaderRight();
|
||||
if (rightNode != null)
|
||||
header.getChildren().add(rightNode);
|
||||
header.getChildren().add(expandIcon);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
this.getChildren().add(headerRippler);
|
||||
|
||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
event.consume();
|
||||
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
boolean expanded = !this.expanded;
|
||||
this.expanded = expanded;
|
||||
if (expanded) {
|
||||
sublist.doLazyInit();
|
||||
|
||||
if (container == null) {
|
||||
this.container = new VBox();
|
||||
|
||||
if (!noPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(sublist);
|
||||
ComponentSublistWrapper.this.getChildren().add(container);
|
||||
|
||||
this.applyCss();
|
||||
}
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
double currentRotate = expandIcon.getRotate();
|
||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||
|
||||
expandAnimation = new Timeline(
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||
);
|
||||
|
||||
expandAnimation.play();
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
expandIcon.setRotate(targetRotate);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class FloatListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
protected final StackPane pane = new StackPane();
|
||||
|
||||
public FloatListCell(JFXListView<T> listView) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
pane.getStyleClass().add("card");
|
||||
pane.setCursor(Cursor.HAND);
|
||||
setPadding(new Insets(9, 9, 0, 9));
|
||||
JFXDepthManager.setDepth(pane, 1);
|
||||
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
pane.pseudoClassStateChanged(SELECTED, selected);
|
||||
});
|
||||
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
setPrefWidth(0);
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
minWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(pane);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateControl(T dataItem, boolean empty);
|
||||
}
|
||||
@@ -72,19 +72,13 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
||||
acceptButton.setOnAction(e -> {
|
||||
acceptPane.showSpinner();
|
||||
|
||||
onResult.call(textField.getText(), new FutureCallback.ResultHandler() {
|
||||
@Override
|
||||
public void resolve() {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(textField.getText());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String reason) {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(reason);
|
||||
}
|
||||
onResult.call(textField.getText(), () -> {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(textField.getText());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, msg -> {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(msg);
|
||||
});
|
||||
});
|
||||
textField.setOnAction(event -> acceptButton.fire());
|
||||
|
||||
@@ -1,123 +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.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent {
|
||||
|
||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||
|
||||
protected final BorderPane root;
|
||||
protected final RipplerContainer container;
|
||||
|
||||
private final Label titleLabel;
|
||||
|
||||
public LineButtonBase() {
|
||||
this.root = new BorderPane();
|
||||
root.setPadding(PADDING);
|
||||
root.setMinHeight(48);
|
||||
|
||||
this.container = new RipplerContainer(root);
|
||||
this.getChildren().setAll(container);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
root.setCenter(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.textProperty().bind(titleProperty());
|
||||
titleLabel.getStyleClass().add("title");
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private VBox left;
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButtonBase.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (left == null) {
|
||||
left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
root.setCenter(left);
|
||||
} else if (left != null) {
|
||||
subtitleLabel.setText(null);
|
||||
root.setCenter(titleLabel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +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.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineNavigationButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-navigation-button";
|
||||
|
||||
public LineNavigationButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
root.setMouseTransparent(true);
|
||||
|
||||
HBox right = new HBox();
|
||||
root.setRight(right);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Label valueLabel = new Label();
|
||||
valueLabel.getStyleClass().add("subtitle");
|
||||
valueLabel.textProperty().bind(messageProperty());
|
||||
|
||||
Node arrowIcon = SVG.ARROW_FORWARD.createIcon(24);
|
||||
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
disabledProperty().addListener((observable, oldValue, newValue) ->
|
||||
arrowIcon.setOpacity(newValue ? 0.4 : 1.0));
|
||||
|
||||
right.getChildren().setAll(valueLabel, arrowIcon);
|
||||
}
|
||||
|
||||
FXUtils.onClicked(container, this::fire);
|
||||
}
|
||||
|
||||
public void fire() {
|
||||
fireEvent(new ActionEvent());
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<>() {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineNavigationButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onAction";
|
||||
}
|
||||
};
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
return onAction;
|
||||
}
|
||||
|
||||
public EventHandler<ActionEvent> getOnAction() {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
public void setOnAction(EventHandler<ActionEvent> value) {
|
||||
onActionProperty().set(value);
|
||||
}
|
||||
|
||||
private final StringProperty message = new SimpleStringProperty(this, "message", "");
|
||||
|
||||
public StringProperty messageProperty() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return messageProperty().get();
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
messageProperty().set(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +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.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/// @author Glavo
|
||||
public class LinePane extends BorderPane implements NoPaddingComponent {
|
||||
|
||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||
|
||||
private final Label titleLabel;
|
||||
|
||||
public LinePane() {
|
||||
this.setPadding(PADDING);
|
||||
this.setMinHeight(48);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
this.setCenter(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.textProperty().bind(titleProperty());
|
||||
titleLabel.getStyleClass().add("title");
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private VBox left;
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LinePane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (left == null) {
|
||||
left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
LinePane.this.setCenter(left);
|
||||
} else if (left != null) {
|
||||
subtitleLabel.setText(null);
|
||||
LinePane.this.setCenter(titleLabel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
}
|
||||
@@ -1,231 +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 com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineSelectButton<T> extends LineButtonBase {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private JFXPopup popup;
|
||||
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
root.setMouseTransparent(true);
|
||||
|
||||
HBox right = new HBox();
|
||||
root.setRight(right);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Label valueLabel = new Label();
|
||||
valueLabel.getStyleClass().add("subtitle");
|
||||
|
||||
InvalidationListener updateValue = observable -> {
|
||||
T value = getValue();
|
||||
if (value == null)
|
||||
valueLabel.setText("");
|
||||
else {
|
||||
Function<T, String> converter = getConverter();
|
||||
valueLabel.setText(converter != null ? converter.apply(value) : value.toString());
|
||||
}
|
||||
};
|
||||
converterProperty().addListener(updateValue);
|
||||
valueProperty().addListener(updateValue);
|
||||
|
||||
Node arrowIcon = SVG.UNFOLD_MORE.createIcon(24);
|
||||
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
right.getChildren().setAll(valueLabel, arrowIcon);
|
||||
}
|
||||
|
||||
container.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (popup == null) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
this.popup = new JFXPopup(popupMenu);
|
||||
|
||||
container.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
itemTitleLabel.getStyleClass().add("title");
|
||||
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (item == null)
|
||||
return "";
|
||||
|
||||
Function<T, String> converter = getConverter();
|
||||
return converter != null ? converter.apply(item) : Objects.toString(item, "");
|
||||
}, converterProperty()));
|
||||
|
||||
var itemSubtitleLabel = new Label();
|
||||
itemSubtitleLabel.getStyleClass().add("subtitle");
|
||||
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
Function<T, String> descriptionConverter = getDescriptionConverter();
|
||||
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
|
||||
}, descriptionConverterProperty()));
|
||||
|
||||
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
|
||||
if (text == null || text.isEmpty()) {
|
||||
vbox.getChildren().setAll(itemTitleLabel);
|
||||
} else {
|
||||
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
|
||||
}
|
||||
});
|
||||
|
||||
var wrapper = new StackPane(vbox);
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("menu-container");
|
||||
wrapper.setMouseTransparent(true);
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
|
||||
FXUtils.onClicked(ripplerContainer, () -> {
|
||||
setValue(item);
|
||||
popup.hide();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(valueProperty(),
|
||||
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
|
||||
|
||||
return ripplerContainer;
|
||||
}));
|
||||
|
||||
popup.showingProperty().addListener((observable, oldValue, newValue) ->
|
||||
container.getRippler().setRipplerDisabled(newValue));
|
||||
}
|
||||
|
||||
if (popup.isShowing()) {
|
||||
popup.hide();
|
||||
} else {
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
|
||||
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight());
|
||||
}
|
||||
|
||||
event.consume();
|
||||
} else if (event.getButton() == MouseButton.SECONDARY) {
|
||||
if (popup != null)
|
||||
popup.hide();
|
||||
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
|
||||
|
||||
public ObjectProperty<T> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return valueProperty().get();
|
||||
}
|
||||
|
||||
public void setValue(T value) {
|
||||
valueProperty().set(value);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Function<T, String>> converter = new SimpleObjectProperty<>(this, "converter");
|
||||
|
||||
public ObjectProperty<Function<T, String>> converterProperty() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
public Function<T, String> getConverter() {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
public void setConverter(Function<T, String> value) {
|
||||
converterProperty().set(value);
|
||||
}
|
||||
|
||||
private ObjectProperty<Function<T, String>> descriptionConverter;
|
||||
|
||||
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
|
||||
if (descriptionConverter == null) {
|
||||
descriptionConverter = new SimpleObjectProperty<>(this, "descriptionConverter");
|
||||
}
|
||||
return descriptionConverter;
|
||||
}
|
||||
|
||||
public Function<T, String> getDescriptionConverter() {
|
||||
return descriptionConverterProperty().get();
|
||||
}
|
||||
|
||||
public void setDescriptionConverter(Function<T, String> value) {
|
||||
descriptionConverterProperty().set(value);
|
||||
}
|
||||
|
||||
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.emptyObservableList());
|
||||
|
||||
public ListProperty<T> itemsProperty() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(ObservableList<T> value) {
|
||||
itemsProperty().set(value);
|
||||
}
|
||||
|
||||
public void setItems(Collection<T> value) {
|
||||
if (value instanceof ObservableList<T> observableList) {
|
||||
this.setItems(observableList);
|
||||
} else {
|
||||
this.setItems(FXCollections.observableArrayList(value));
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void setItems(T... values) {
|
||||
this.setItems(FXCollections.observableArrayList(values));
|
||||
}
|
||||
|
||||
public ObservableList<T> getItems() {
|
||||
return items.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +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.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineTextPane extends LinePane {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-label-pane";
|
||||
|
||||
public LineTextPane() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
private StringProperty text;
|
||||
|
||||
public StringProperty textProperty() {
|
||||
if (text == null) {
|
||||
text = new StringPropertyBase() {
|
||||
private static final Insets LABEL_MARGIN = new Insets(0, 8, 0, 16);
|
||||
|
||||
private Label rightLabel;
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineTextPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "text";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String text = get();
|
||||
if (text == null || text.isEmpty()) {
|
||||
if (rightLabel != null)
|
||||
rightLabel.setText(null);
|
||||
LineTextPane.this.setRight(null);
|
||||
} else {
|
||||
if (rightLabel == null) {
|
||||
rightLabel = FXUtils.newSafeTruncatedLabel();
|
||||
FXUtils.copyOnDoubleClick(rightLabel);
|
||||
BorderPane.setMargin(rightLabel, LABEL_MARGIN);
|
||||
BorderPane.setAlignment(rightLabel, Pos.CENTER_RIGHT);
|
||||
}
|
||||
rightLabel.setText(text);
|
||||
LineTextPane.this.setRight(rightLabel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return textProperty().get();
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
textProperty().set(text);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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 com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class LineToggleButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-toggle-button";
|
||||
|
||||
public LineToggleButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
toggleButton.selectedProperty().bindBidirectional(selectedProperty());
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
BorderPane.setAlignment(toggleButton, Pos.CENTER);
|
||||
root.setRight(toggleButton);
|
||||
|
||||
FXUtils.onClicked(container, toggleButton::fire);
|
||||
}
|
||||
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selectedProperty().get();
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
selectedProperty().set(selected);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXColorPicker;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiColorItem extends ComponentList {
|
||||
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
|
||||
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
|
||||
|
||||
private final ToggleGroup group = new ToggleGroup();
|
||||
private final JFXColorPicker colorPicker = new JFXColorPicker();
|
||||
private final JFXRadioButton radioCustom = new JFXRadioButton();
|
||||
private final BorderPane custom = new BorderPane();
|
||||
private final VBox pane = new VBox();
|
||||
private final boolean hasCustom;
|
||||
|
||||
private Consumer<Toggle> toggleSelectedListener;
|
||||
private Consumer<Color> colorConsumer;
|
||||
|
||||
public MultiColorItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
|
||||
this.hasCustom = hasCustom;
|
||||
|
||||
radioCustom.textProperty().bind(customTitleProperty());
|
||||
radioCustom.setToggleGroup(group);
|
||||
colorPicker.disableProperty().bind(radioCustom.selectedProperty().not());
|
||||
|
||||
colorPicker.setOnAction(e -> Optional.ofNullable(colorConsumer).ifPresent(c -> c.accept(colorPicker.getValue())));
|
||||
|
||||
custom.setLeft(radioCustom);
|
||||
custom.setStyle("-fx-padding: 3;");
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(3);
|
||||
right.getChildren().addAll(colorPicker);
|
||||
custom.setRight(right);
|
||||
FXUtils.setLimitHeight(custom, 40);
|
||||
|
||||
pane.setStyle("-fx-padding: 0 0 10 0;");
|
||||
pane.setSpacing(8);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
getContent().add(pane);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
if (toggleSelectedListener != null)
|
||||
toggleSelectedListener.accept(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
public Node createChildren(String title) {
|
||||
return createChildren(title, null);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, Object userData) {
|
||||
return createChildren(title, "", userData);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, String subtitle, Object userData) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setStyle("-fx-padding: 3;");
|
||||
|
||||
JFXRadioButton left = new JFXRadioButton(title);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(userData);
|
||||
pane.setLeft(left);
|
||||
|
||||
Label right = new Label(subtitle);
|
||||
right.getStyleClass().add("subtitle-label");
|
||||
right.setStyle("-fx-font-size: 10;");
|
||||
pane.setRight(right);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
public void loadChildren(Collection<Node> list) {
|
||||
pane.getChildren().setAll(list);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
}
|
||||
|
||||
public ToggleGroup getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getCustomTitle() {
|
||||
return customTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty customTitleProperty() {
|
||||
return customTitle;
|
||||
}
|
||||
|
||||
public void setCustomTitle(String customTitle) {
|
||||
this.customTitle.set(customTitle);
|
||||
}
|
||||
|
||||
public String getChooserTitle() {
|
||||
return chooserTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty chooserTitleProperty() {
|
||||
return chooserTitle;
|
||||
}
|
||||
|
||||
public void setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle.set(chooserTitle);
|
||||
}
|
||||
|
||||
public void setCustomUserData(Object userData) {
|
||||
radioCustom.setUserData(userData);
|
||||
}
|
||||
|
||||
public boolean isCustomToggle(Toggle toggle) {
|
||||
return radioCustom == toggle;
|
||||
}
|
||||
|
||||
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
|
||||
toggleSelectedListener = consumer;
|
||||
}
|
||||
|
||||
public void setOnColorPickerChanged(Consumer<Color> consumer) {
|
||||
colorConsumer = consumer;
|
||||
}
|
||||
|
||||
public void setColor(Color color) {
|
||||
colorPicker.setValue(color);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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;
|
||||
|
||||
/// Marker interface for no padding in [ComponentList].
|
||||
///
|
||||
/// @author Glavo
|
||||
interface NoPaddingComponent {
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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 com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class OptionToggleButton extends StackPane {
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||
|
||||
public OptionToggleButton() {
|
||||
getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 8, 8, 16));
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
getChildren().setAll(container);
|
||||
|
||||
VBox left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMouseTransparent(true);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
subtitleLabel.textProperty().bind(subtitle);
|
||||
pane.setCenter(left);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
StackPane right = new StackPane(toggleButton);
|
||||
right.setAlignment(Pos.CENTER);
|
||||
pane.setRight(right);
|
||||
toggleButton.selectedProperty().bindBidirectional(selected);
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
} else {
|
||||
left.getChildren().setAll(titleLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
}
|
||||
@@ -1,236 +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.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.css.converter.InsetsConverter;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
// TODO: We plan to replace ComponentList with this class, but we need to address some issues first
|
||||
|
||||
/// @author Glavo
|
||||
public final class OptionsList extends Control {
|
||||
public OptionsList() {
|
||||
this.getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new OptionsListSkin(this);
|
||||
}
|
||||
|
||||
private final ObservableList<Element> elements = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<Element> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
public void addTitle(String title) {
|
||||
elements.add(new Title(title));
|
||||
}
|
||||
|
||||
public void addNode(Node node) {
|
||||
elements.add(new NodeElement(node));
|
||||
}
|
||||
|
||||
public void addListElement(@NotNull Node node) {
|
||||
elements.add(new ListElement(node));
|
||||
}
|
||||
|
||||
public void addListElements(@NotNull Node... nodes) {
|
||||
for (Node node : nodes) {
|
||||
elements.add(new ListElement(node));
|
||||
}
|
||||
}
|
||||
|
||||
private final StyleableObjectProperty<Insets> contentPadding = new StyleableObjectProperty<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return OptionsList.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "contentPadding";
|
||||
}
|
||||
|
||||
@Override
|
||||
public javafx.css.CssMetaData<OptionsList, Insets> getCssMetaData() {
|
||||
return StyleableProperties.CONTENT_PADDING;
|
||||
}
|
||||
};
|
||||
|
||||
public StyleableObjectProperty<Insets> contentPaddingProperty() {
|
||||
return contentPadding;
|
||||
}
|
||||
|
||||
public Insets getContentPadding() {
|
||||
return contentPaddingProperty().get();
|
||||
}
|
||||
|
||||
public void setContentPadding(Insets padding) {
|
||||
contentPaddingProperty().set(padding);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<OptionsList, Insets> CONTENT_PADDING = new CssMetaData<>("-jfx-content-padding", InsetsConverter.getInstance()) {
|
||||
@Override
|
||||
public boolean isSettable(OptionsList styleable) {
|
||||
return styleable.contentPadding == null || !styleable.contentPadding.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Insets> getStyleableProperty(OptionsList styleable) {
|
||||
return styleable.contentPaddingProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, CONTENT_PADDING);
|
||||
STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static abstract class Element {
|
||||
protected Node node;
|
||||
|
||||
Node getNode() {
|
||||
if (node == null)
|
||||
node = createNode();
|
||||
return node;
|
||||
}
|
||||
|
||||
protected abstract Node createNode();
|
||||
}
|
||||
|
||||
public static final class Title extends Element {
|
||||
private final @NotNull String title;
|
||||
|
||||
public Title(@NotNull String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
Label label = new Label(title);
|
||||
label.setPadding(new Insets(8, 0, 8, 0));
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof Title that && Objects.equals(this.title, that.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return title.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Title[%s]".formatted(title);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class NodeElement extends Element {
|
||||
public NodeElement(@NotNull Node node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof NodeElement that && this.node.equals(that.node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeElement[node=%s]".formatted(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ListElement extends Element {
|
||||
private final Node original;
|
||||
|
||||
public ListElement(@NotNull Node node) {
|
||||
this.original = node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
if (original instanceof ComponentSublist sublist) {
|
||||
return new ComponentSublistWrapper(sublist);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof ListElement that && this.original.equals(that.original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return original.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ListElement[node=%s]".formatted(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +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 com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public final class OptionsListSkin extends SkinBase<OptionsList> {
|
||||
|
||||
private final JFXListView<OptionsList.Element> listView;
|
||||
private final ObjectBinding<ContentPaddings> contentPaddings;
|
||||
|
||||
OptionsListSkin(OptionsList control) {
|
||||
super(control);
|
||||
|
||||
this.listView = new JFXListView<>();
|
||||
listView.setItems(control.getElements());
|
||||
listView.setCellFactory(listView1 -> new Cell());
|
||||
|
||||
this.contentPaddings = Bindings.createObjectBinding(() -> {
|
||||
Insets padding = control.getContentPadding();
|
||||
return padding == null ? ContentPaddings.EMPTY : new ContentPaddings(
|
||||
new Insets(padding.getTop(), padding.getRight(), 0, padding.getLeft()),
|
||||
new Insets(0, padding.getRight(), padding.getBottom(), padding.getLeft()),
|
||||
new Insets(0, padding.getRight(), 0, padding.getLeft())
|
||||
);
|
||||
}, control.contentPaddingProperty());
|
||||
|
||||
this.getChildren().setAll(listView);
|
||||
}
|
||||
|
||||
private record ContentPaddings(Insets first, Insets last, Insets middle) {
|
||||
static final ContentPaddings EMPTY = new ContentPaddings(Insets.EMPTY, Insets.EMPTY, Insets.EMPTY);
|
||||
}
|
||||
|
||||
private final class Cell extends ListCell<OptionsList.Element> {
|
||||
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener updateStyleListener = o -> updateStyle();
|
||||
|
||||
private StackPane wrapper;
|
||||
|
||||
public Cell() {
|
||||
FXUtils.limitCellWidth(listView, this);
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(updateStyleListener);
|
||||
listView.itemsProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (oldValue != null)
|
||||
oldValue.removeListener(weakListener);
|
||||
if (newValue != null)
|
||||
newValue.addListener(weakListener);
|
||||
|
||||
weakListener.invalidated(o);
|
||||
});
|
||||
itemProperty().addListener(weakListener);
|
||||
contentPaddings.addListener(weakListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(OptionsList.Element item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else if (item instanceof OptionsList.ListElement element) {
|
||||
if (wrapper == null)
|
||||
wrapper = createWrapper();
|
||||
else
|
||||
wrapper.getStyleClass().remove("no-padding");
|
||||
|
||||
Node node = element.getNode();
|
||||
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding"))
|
||||
wrapper.getStyleClass().add("no-padding");
|
||||
|
||||
wrapper.getChildren().setAll(node);
|
||||
|
||||
setGraphic(wrapper);
|
||||
} else {
|
||||
setGraphic(item.getNode());
|
||||
}
|
||||
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
private StackPane createWrapper() {
|
||||
var wrapper = new StackPane();
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("options-list-item");
|
||||
updateStyle();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void updateStyle() {
|
||||
OptionsList.Element item = getItem();
|
||||
int index = getIndex();
|
||||
ObservableList<OptionsList.Element> items = getListView().getItems();
|
||||
|
||||
if (item == null || index < 0 || index >= items.size()) {
|
||||
this.setPadding(Insets.EMPTY);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isFirst = index == 0;
|
||||
boolean isLast = index == items.size() - 1;
|
||||
|
||||
ContentPaddings paddings = contentPaddings.get();
|
||||
if (isFirst) {
|
||||
this.setPadding(paddings.first);
|
||||
} else if (isLast) {
|
||||
this.setPadding(paddings.last);
|
||||
} else {
|
||||
this.setPadding(paddings.middle);
|
||||
}
|
||||
|
||||
if (item instanceof OptionsList.ListElement && wrapper != null) {
|
||||
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, isFirst || !(items.get(index - 1) instanceof OptionsList.ListElement));
|
||||
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_LAST, isLast || !(items.get(index + 1) instanceof OptionsList.ListElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -78,8 +78,6 @@ public class PopupMenu extends Control {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.vbarPolicyProperty().bind(new When(alwaysShowingVBar)
|
||||
.then(ScrollPane.ScrollBarPolicy.ALWAYS)
|
||||
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
|
||||
|
||||
@@ -117,17 +117,11 @@ public class PromptDialogPane extends DialogPane {
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
|
||||
builder.callback.call(builder.questions, new FutureCallback.ResultHandler() {
|
||||
@Override
|
||||
public void resolve() {
|
||||
future.complete(builder.questions);
|
||||
runInFX(() -> onSuccess());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String reason) {
|
||||
runInFX(() -> onFailure(reason));
|
||||
}
|
||||
builder.callback.call(builder.questions, () -> {
|
||||
future.complete(builder.questions);
|
||||
runInFX(this::onSuccess);
|
||||
}, msg -> {
|
||||
runInFX(() -> onFailure(msg));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,17 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
@@ -32,41 +37,39 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@DefaultProperty("container")
|
||||
public class RipplerContainer extends StackPane {
|
||||
private static final String DEFAULT_STYLE_CLASS = "rippler-container";
|
||||
private static final CornerRadii DEFAULT_RADII = new CornerRadii(3);
|
||||
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
|
||||
private static final Duration DURATION = Duration.millis(200);
|
||||
|
||||
private final Node container;
|
||||
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null);
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL, this, "ripplerFill", null);
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
|
||||
|
||||
private final StackPane buttonContainer = new StackPane();
|
||||
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
|
||||
private static final Background DEFAULT_MASK_BACKGROUND = new Background(new BackgroundFill(Color.WHITE, DEFAULT_RADII, Insets.EMPTY));
|
||||
|
||||
@Override
|
||||
protected Node getMask() {
|
||||
StackPane mask = new StackPane();
|
||||
mask.shapeProperty().bind(buttonContainer.shapeProperty());
|
||||
mask.setBackground(DEFAULT_MASK_BACKGROUND);
|
||||
mask.resize(
|
||||
buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(),
|
||||
buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset()
|
||||
);
|
||||
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(new BackgroundFill(Color.WHITE, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getRadii() : defaultRadii, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)), buttonContainer.backgroundProperty()));
|
||||
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
private Transition coverAnimation;
|
||||
private final CornerRadii defaultRadii = new CornerRadii(3);
|
||||
|
||||
public RipplerContainer(Node container) {
|
||||
this.container = container;
|
||||
public RipplerContainer(@NamedArg("container") Node container) {
|
||||
setContainer(container);
|
||||
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
|
||||
@@ -83,79 +86,71 @@ public class RipplerContainer extends StackPane {
|
||||
setPickOnBounds(false);
|
||||
|
||||
buttonContainer.setPickOnBounds(false);
|
||||
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
|
||||
|
||||
containerProperty().addListener(o -> updateChildren());
|
||||
updateChildren();
|
||||
|
||||
var shape = new Rectangle();
|
||||
shape.widthProperty().bind(widthProperty());
|
||||
shape.heightProperty().bind(heightProperty());
|
||||
setShape(shape);
|
||||
InvalidationListener listener = o -> {
|
||||
if (isSelected()) setBackground(new Background(new BackgroundFill(getRipplerFill(), defaultRadii, null)));
|
||||
else setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
|
||||
};
|
||||
|
||||
selectedProperty().addListener(listener);
|
||||
selectedProperty().addListener((a, b, newValue) ->
|
||||
pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), newValue));
|
||||
ripplerFillProperty().addListener(listener);
|
||||
|
||||
setShape(Lang.apply(new Rectangle(), rectangle -> {
|
||||
rectangle.widthProperty().bind(widthProperty());
|
||||
rectangle.heightProperty().bind(heightProperty());
|
||||
}));
|
||||
|
||||
EventHandler<MouseEvent> mouseEventHandler;
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
mouseEventHandler = event -> {
|
||||
if (coverAnimation != null) {
|
||||
coverAnimation.stop();
|
||||
coverAnimation = null;
|
||||
setOnMouseEntered(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Motion.EASE_IN);
|
||||
}
|
||||
|
||||
if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
|
||||
coverAnimation = new Transition() {
|
||||
{
|
||||
setCycleDuration(Motion.SHORT4);
|
||||
setInterpolator(Motion.EASE_IN);
|
||||
}
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(frac);
|
||||
}
|
||||
}.play());
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(frac);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
coverAnimation = new Transition() {
|
||||
{
|
||||
setCycleDuration(Motion.SHORT4);
|
||||
setInterpolator(Motion.EASE_OUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(1 - frac);
|
||||
}
|
||||
};
|
||||
setOnMouseExited(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Motion.EASE_OUT);
|
||||
}
|
||||
|
||||
coverAnimation.play();
|
||||
};
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(1 - frac);
|
||||
}
|
||||
}.play());
|
||||
} else {
|
||||
mouseEventHandler = event ->
|
||||
interpolateBackground(event.getEventType() == MouseEvent.MOUSE_ENTERED ? 1 : 0);
|
||||
setOnMouseEntered(e -> interpolateBackground(1));
|
||||
setOnMouseExited(e -> interpolateBackground(0));
|
||||
}
|
||||
|
||||
addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEventHandler);
|
||||
addEventHandler(MouseEvent.MOUSE_EXITED, mouseEventHandler);
|
||||
}
|
||||
|
||||
private void interpolateBackground(double frac) {
|
||||
if (frac < 0.01) {
|
||||
setBackground(null);
|
||||
} else {
|
||||
Color onSurface = Themes.getColorScheme().getOnSurface();
|
||||
setBackground(new Background(new BackgroundFill(
|
||||
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
|
||||
CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
Color onSurface = Themes.getColorScheme().getOnSurface();
|
||||
setBackground(new Background(new BackgroundFill(
|
||||
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
|
||||
CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
Node container = getContainer();
|
||||
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK) {
|
||||
getChildren().setAll(buttonContainer, container);
|
||||
container.setPickOnBounds(false);
|
||||
} else {
|
||||
getChildren().setAll(container, buttonContainer);
|
||||
buttonContainer.setPickOnBounds(false);
|
||||
}
|
||||
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK)
|
||||
getChildren().setAll(buttonContainer, getContainer());
|
||||
else
|
||||
getChildren().setAll(getContainer(), buttonContainer);
|
||||
|
||||
for (int i = 1; i < getChildren().size(); ++i)
|
||||
getChildren().get(i).setPickOnBounds(false);
|
||||
}
|
||||
|
||||
public void setPosition(JFXRippler.RipplerPos pos) {
|
||||
@@ -163,46 +158,40 @@ public class RipplerContainer extends StackPane {
|
||||
updateChildren();
|
||||
}
|
||||
|
||||
public JFXRippler getRippler() {
|
||||
return buttonRippler;
|
||||
public Node getContainer() {
|
||||
return container.get();
|
||||
}
|
||||
|
||||
public Node getContainer() {
|
||||
public ObjectProperty<Node> containerProperty() {
|
||||
return container;
|
||||
}
|
||||
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new StyleableObjectProperty<>(DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return RipplerContainer.this;
|
||||
}
|
||||
public void setContainer(Node container) {
|
||||
this.container.set(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ripplerFill";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CssMetaData<? extends Styleable, Paint> getCssMetaData() {
|
||||
return StyleableProperties.RIPPLER_FILL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
buttonRippler.setRipplerFill(get());
|
||||
}
|
||||
};
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
||||
return ripplerFill;
|
||||
}
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFillProperty().get();
|
||||
public void setRipplerFill(Paint ripplerFill) {
|
||||
this.ripplerFill.set(ripplerFill);
|
||||
}
|
||||
|
||||
public void setRipplerFill(Paint ripplerFill) {
|
||||
ripplerFillProperty().set(ripplerFill);
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -211,28 +200,12 @@ public class RipplerContainer extends StackPane {
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<>("-jfx-rippler-fill", PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public boolean isSettable(RipplerContainer styleable) {
|
||||
return styleable.ripplerFill == null || !styleable.ripplerFill.isBound();
|
||||
}
|
||||
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer styleable) {
|
||||
return styleable.ripplerFillProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
|
||||
styleables.add(RIPPLER_FILL);
|
||||
STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
@@ -165,8 +164,6 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom")));
|
||||
} else if (task instanceof LegacyFabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.legacyfabric")));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
@@ -326,7 +323,8 @@ public final class TaskListPane extends StackPane {
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
|
||||
if (item instanceof ProgressListNode progressListNode) {
|
||||
if (item instanceof ProgressListNode) {
|
||||
var progressListNode = (ProgressListNode) item;
|
||||
title.setText(progressListNode.title);
|
||||
message.textProperty().bind(progressListNode.message);
|
||||
bar.progressProperty().bind(progressListNode.progress);
|
||||
@@ -335,7 +333,8 @@ public final class TaskListPane extends StackPane {
|
||||
pane.setLeft(null);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(bar);
|
||||
} else if (item instanceof StageNode stageNode) {
|
||||
} else if (item instanceof StageNode) {
|
||||
var stageNode = (StageNode) item;
|
||||
title.textProperty().bind(stageNode.title);
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
@@ -428,25 +427,23 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
// CHECKSTYLE:OFF
|
||||
// @formatter:off
|
||||
message = switch (stageKey) {
|
||||
case "hmcl.modpack" -> i18n("install.modpack");
|
||||
case "hmcl.modpack.download" -> i18n("launch.state.modpack");
|
||||
case "hmcl.install.assets" -> i18n("assets.download");
|
||||
case "hmcl.install.libraries" -> i18n("libraries.download");
|
||||
case "hmcl.install.game" -> i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue);
|
||||
case "hmcl.install.forge" -> i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue);
|
||||
case "hmcl.install.cleanroom" -> i18n("install.installer.install", i18n("install.installer.cleanroom") + " " + stageValue);
|
||||
case "hmcl.install.neoforge" -> i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue);
|
||||
case "hmcl.install.liteloader" -> i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue);
|
||||
case "hmcl.install.optifine" -> i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue);
|
||||
case "hmcl.install.fabric" -> i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue);
|
||||
case "hmcl.install.fabric-api" -> i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue);
|
||||
case "hmcl.install.legacyfabric" -> i18n("install.installer.install", i18n("install.installer.legacyfabric") + " " + stageValue);
|
||||
case "hmcl.install.legacyfabric-api" -> i18n("install.installer.install", i18n("install.installer.legacyfabric-api") + " " + stageValue);
|
||||
case "hmcl.install.quilt" -> i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue);
|
||||
case "hmcl.install.quilt-api" -> i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue);
|
||||
default -> i18n(stageKey);
|
||||
};
|
||||
switch (stageKey) {
|
||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.libraries": message = i18n("libraries.download"); break;
|
||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||
case "hmcl.install.cleanroom": message = i18n("install.installer.install", i18n("install.installer.cleanroom") + " " + stageValue); break;
|
||||
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
||||
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
}
|
||||
// @formatter:on
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
@@ -491,10 +488,12 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
private ProgressListNode(Task<?> task) {
|
||||
this.title = task.getName();
|
||||
message.bind(task.messageProperty());
|
||||
progress.bind(task.progressProperty());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
message.unbind();
|
||||
progress.unbind();
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,6 @@ public class TwoLineListItem extends VBox {
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final Label lblSubtitle;
|
||||
private final Label lblTitle;
|
||||
|
||||
private final AggregatedObservableList<Node> firstLineChildren;
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
@@ -62,7 +59,7 @@ public class TwoLineListItem extends VBox {
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
|
||||
lblTitle = new Label();
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
@@ -71,7 +68,7 @@ public class TwoLineListItem extends VBox {
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
|
||||
lblSubtitle = new Label();
|
||||
Label lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
|
||||
@@ -112,14 +109,6 @@ public class TwoLineListItem extends VBox {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public Label getSubtitleLabel() {
|
||||
return lblSubtitle;
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
|
||||
@@ -328,18 +328,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
}
|
||||
if (onTitleBarDoubleClick != null)
|
||||
center.setOnMouseClicked(onTitleBarDoubleClick);
|
||||
center.setOnMouseDragged(mouseEvent -> {
|
||||
if (!getSkinnable().isDragging() && primaryStage.isMaximized()) {
|
||||
getSkinnable().setDragging(true);
|
||||
mouseInitX = mouseEvent.getScreenX();
|
||||
mouseInitY = mouseEvent.getScreenY();
|
||||
primaryStage.setMaximized(false);
|
||||
stageInitWidth = primaryStage.getWidth();
|
||||
stageInitHeight = primaryStage.getHeight();
|
||||
primaryStage.setY(stageInitY = 0);
|
||||
primaryStage.setX(stageInitX = mouseInitX - stageInitWidth / 2);
|
||||
}
|
||||
});
|
||||
navBar.setCenter(center);
|
||||
|
||||
if (canRefresh) {
|
||||
|
||||
@@ -24,12 +24,12 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -38,11 +38,9 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public abstract class AbstractInstallersPage extends Control implements WizardPage {
|
||||
public static final String FABRIC_QUILT_API_TIP = "fabricQuiltApi";
|
||||
protected final WizardController controller;
|
||||
|
||||
protected InstallerItem.InstallerItemGroup group;
|
||||
@@ -58,15 +56,8 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.setOnInstall(() -> {
|
||||
if (!Boolean.TRUE.equals(config().getShownTips().get(FABRIC_QUILT_API_TIP))
|
||||
&& (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)
|
||||
|| LibraryAnalyzer.LibraryType.QUILT_API.getPatchId().equals(libraryId)
|
||||
|| LibraryAnalyzer.LibraryType.LEGACY_FABRIC_API.getPatchId().equals(libraryId))) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.installer.fabric-quilt-api.warning", i18n("install.installer." + libraryId)),
|
||||
i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.WARNING
|
||||
).ok(null).addCancel(i18n("button.do_not_show_again"), () -> config().getShownTips().put(FABRIC_QUILT_API_TIP, true)).build());
|
||||
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
|
||||
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
|
||||
@@ -134,7 +134,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
@@ -152,7 +152,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Controllers.showToast(i18n("install.success"));
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
|
||||
}
|
||||
|
||||
@@ -121,20 +121,32 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
|
||||
if (libraryType != null) {
|
||||
String loaderName = switch (libraryType) {
|
||||
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)
|
||||
continue;
|
||||
String loaderName;
|
||||
switch (libraryType) {
|
||||
case FORGE:
|
||||
loaderName = i18n("install.installer.forge");
|
||||
break;
|
||||
case NEO_FORGE:
|
||||
loaderName = i18n("install.installer.neoforge");
|
||||
break;
|
||||
case CLEANROOM:
|
||||
loaderName = i18n("install.installer.cleanroom");
|
||||
break;
|
||||
case FABRIC:
|
||||
loaderName = i18n("install.installer.fabric");
|
||||
break;
|
||||
case LITELOADER:
|
||||
loaderName = i18n("install.installer.liteloader");
|
||||
break;
|
||||
case QUILT:
|
||||
loaderName = i18n("install.installer.quilt");
|
||||
break;
|
||||
case OPTIFINE:
|
||||
loaderName = i18n("install.installer.optifine");
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
nameBuilder.append("-").append(loaderName);
|
||||
}
|
||||
|
||||
@@ -124,37 +124,37 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
private void onChooseRemoteFile() {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, handler) -> {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
|
||||
try {
|
||||
if (url.endsWith("server-manifest.json")) {
|
||||
// if urlString ends with .json, we assume that the url is server-manifest.json
|
||||
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
handler.reject(i18n("modpack.type.server.malformed"));
|
||||
reject.accept(i18n("modpack.type.server.malformed"));
|
||||
} else if (e == null) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest);
|
||||
controller.onNext();
|
||||
} else {
|
||||
handler.reject(e.getMessage());
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
}).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();
|
||||
resolve.run();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
controller.getSettings().put(MODPACK_FILE, modpack);
|
||||
controller.onNext();
|
||||
} else {
|
||||
handler.reject(e.getMessage());
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
}).executor(true),
|
||||
i18n("message.downloading"),
|
||||
@@ -162,7 +162,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handler.reject(e.getMessage());
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
}, "", new URLValidator());
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
@@ -272,8 +270,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.LEGACY_FABRIC;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
|
||||
@@ -22,10 +22,10 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.Config;
|
||||
import org.jackhuang.hmcl.setting.FontManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -41,9 +41,7 @@ import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@@ -134,9 +132,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ".hmcl/hmcl.json");
|
||||
zip.putFile(tempModpack, ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH.equals(modpackType)
|
||||
? "modpack.mrpack"
|
||||
: "modpack.zip");
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
Path bg = Metadata.HMCL_CURRENT_DIRECTORY.resolve("background");
|
||||
if (!Files.isDirectory(bg))
|
||||
@@ -267,10 +263,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
profile.getRepository(),
|
||||
version,
|
||||
exportInfo,
|
||||
modpackFile
|
||||
profile.getRepository(),
|
||||
version,
|
||||
exportInfo,
|
||||
modpackFile
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private void onNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
|
||||
if (!packWithLauncher.get() && controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
|
||||
if (controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.mrpack"));
|
||||
fileChooser.setInitialFileName(name.get() + (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ".mrpack"));
|
||||
} else {
|
||||
|
||||
@@ -31,15 +31,15 @@ import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
|
||||
public class DownloadSettingsPage extends StackPane {
|
||||
|
||||
@@ -55,37 +55,51 @@ public class DownloadSettingsPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
{
|
||||
var downloadSource = new ComponentList();
|
||||
VBox downloadSource = new VBox(8);
|
||||
downloadSource.getStyleClass().add("card-non-transparent");
|
||||
{
|
||||
|
||||
var autoChooseDownloadSource = new LineToggleButton();
|
||||
autoChooseDownloadSource.setTitle(i18n("settings.launcher.download_source.auto"));
|
||||
autoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
|
||||
VBox chooseWrapper = new VBox();
|
||||
chooseWrapper.setPadding(new Insets(8, 0, 8, 0));
|
||||
JFXCheckBox chkAutoChooseDownloadSource = new JFXCheckBox(i18n("settings.launcher.download_source.auto"));
|
||||
chkAutoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
|
||||
chooseWrapper.getChildren().setAll(chkAutoChooseDownloadSource);
|
||||
|
||||
Function<String, String> converter = key -> i18n("download.provider." + key);
|
||||
Function<String, String> descriptionConverter = key -> {
|
||||
String bundleKey = "download.provider." + key + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
};
|
||||
BorderPane versionListSourcePane = new BorderPane();
|
||||
versionListSourcePane.setPadding(new Insets(0, 0, 8, 30));
|
||||
versionListSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty().not());
|
||||
{
|
||||
Label label = new Label(i18n("settings.launcher.version_list_source"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
versionListSourcePane.setLeft(label);
|
||||
|
||||
var versionListSourcePane = new LineSelectButton<String>();
|
||||
versionListSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty().not());
|
||||
versionListSourcePane.setTitle(i18n("settings.launcher.version_list_source"));
|
||||
versionListSourcePane.setConverter(converter);
|
||||
versionListSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
versionListSourcePane.setItems(DownloadProviders.AUTO_PROVIDERS.keySet());
|
||||
versionListSourcePane.valueProperty().bindBidirectional(config().versionListSourceProperty());
|
||||
JFXComboBox<String> cboVersionListSource = new JFXComboBox<>();
|
||||
cboVersionListSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
|
||||
versionListSourcePane.setRight(cboVersionListSource);
|
||||
FXUtils.setLimitWidth(cboVersionListSource, 400);
|
||||
|
||||
var downloadSourcePane = new LineSelectButton<String>();
|
||||
downloadSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty());
|
||||
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
|
||||
downloadSourcePane.setConverter(converter);
|
||||
downloadSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
|
||||
cboVersionListSource.getItems().setAll(DownloadProviders.AUTO_PROVIDERS.keySet());
|
||||
selectedItemPropertyFor(cboVersionListSource).bindBidirectional(config().versionListSourceProperty());
|
||||
}
|
||||
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
|
||||
BorderPane downloadSourcePane = new BorderPane();
|
||||
downloadSourcePane.setPadding(new Insets(0, 0, 8, 30));
|
||||
downloadSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty());
|
||||
{
|
||||
Label label = new Label(i18n("settings.launcher.download_source"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
downloadSourcePane.setLeft(label);
|
||||
|
||||
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
|
||||
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
|
||||
downloadSourcePane.setRight(cboDownloadSource);
|
||||
FXUtils.setLimitWidth(cboDownloadSource, 420);
|
||||
|
||||
cboDownloadSource.getItems().setAll(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
|
||||
}
|
||||
|
||||
downloadSource.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
|
||||
@@ -38,10 +38,7 @@ import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.java.JavaInfo;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
@@ -119,7 +116,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
vbox.getChildren().add(prompt);
|
||||
|
||||
for (GameJavaVersion version : supportedGameJavaVersions) {
|
||||
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
|
||||
JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion());
|
||||
button.setUserData(version);
|
||||
vbox.getChildren().add(button);
|
||||
toggleGroup.getToggles().add(button);
|
||||
@@ -251,7 +248,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
|
||||
for (int i = 0; i < versions.size(); i++) {
|
||||
DiscoJavaRemoteVersion version = versions.get(i);
|
||||
if (version.getJdkVersion() == GameJavaVersion.LATEST.majorVersion()) {
|
||||
if (version.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) {
|
||||
remoteVersionBox.getSelectionModel().select(i);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -108,14 +110,17 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
|
||||
|
||||
{
|
||||
var namePane = new LinePane();
|
||||
BorderPane namePane = new BorderPane();
|
||||
{
|
||||
namePane.setTitle(i18n("java.install.name"));
|
||||
Label label = new Label(i18n("java.install.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
namePane.setLeft(label);
|
||||
|
||||
nameField = new JFXTextField();
|
||||
nameField.textProperty().bindBidirectional(control.nameProperty);
|
||||
FXUtils.setLimitWidth(nameField, 200);
|
||||
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
|
||||
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
|
||||
namePane.setRight(nameField);
|
||||
nameField.setValidators(
|
||||
new RequiredValidator(),
|
||||
@@ -171,9 +176,16 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
}
|
||||
|
||||
private void addInfo(String name, String value) {
|
||||
LineTextPane pane = new LineTextPane();
|
||||
pane.setTitle(name);
|
||||
pane.setText(value);
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
pane.setLeft(new Label(name));
|
||||
|
||||
Label valueLabel = FXUtils.newSafeTruncatedLabel(value);
|
||||
FXUtils.copyOnDoubleClick(valueLabel);
|
||||
BorderPane.setMargin(valueLabel, new Insets(0, 0, 0, 16));
|
||||
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
|
||||
pane.setCenter(valueLabel);
|
||||
|
||||
this.componentList.getContent().add(pane);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,6 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
JavaPageSkin(JavaManagementPage skinnable) {
|
||||
super(skinnable);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -211,7 +210,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
@Override
|
||||
protected ListCell<JavaRuntime> createListCell(JFXListView<JavaRuntime> listView) {
|
||||
return new JavaItemCell(listView);
|
||||
return new JavaItemCell();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +222,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
private final StackPane removeIconPane;
|
||||
private final Tooltip removeTooltip = new Tooltip();
|
||||
|
||||
JavaItemCell(JFXListView<JavaRuntime> listView) {
|
||||
JavaItemCell() {
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
HBox center = new HBox();
|
||||
@@ -273,8 +272,6 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
this.graphic = new RipplerContainer(root);
|
||||
|
||||
FXUtils.limitCellWidth(listView, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -59,6 +59,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
@@ -153,13 +154,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
announcementCard.getStyleClass().addAll("card", "announcement");
|
||||
|
||||
VBox announcementBox = new VBox(16);
|
||||
announcementBox.setPadding(new Insets(15));
|
||||
announcementBox.getChildren().add(announcementCard);
|
||||
|
||||
announcementPane = new TransitionPane();
|
||||
announcementPane.setContent(announcementBox, ContainerAnimations.NONE);
|
||||
|
||||
StackPane.setMargin(announcementPane, new Insets(-15));
|
||||
getChildren().add(announcementPane);
|
||||
}
|
||||
|
||||
@@ -247,14 +246,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
menuButton = new JFXButton();
|
||||
menuButton.getStyleClass().add("menu-button");
|
||||
menuButton.setOnAction(e -> GameListPopupMenu.show(
|
||||
menuButton,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
-menuButton.getHeight(),
|
||||
profile, versions
|
||||
));
|
||||
menuButton.setOnAction(e -> onMenu());
|
||||
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
|
||||
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
|
||||
|
||||
@@ -350,6 +342,19 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
private void onMenu() {
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(
|
||||
menuButton,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
-menuButton.getHeight()
|
||||
);
|
||||
}
|
||||
|
||||
private void onUpgrade() {
|
||||
RemoteVersion target = UpdateChecker.getLatestVersion();
|
||||
if (target == null) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.When;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
@@ -43,6 +44,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -77,13 +79,22 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
ComponentList themeList = new ComponentList();
|
||||
{
|
||||
var brightnessPane = new LineSelectButton<String>();
|
||||
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
|
||||
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness." + name));
|
||||
brightnessPane.setItems("auto", "light", "dark");
|
||||
brightnessPane.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
|
||||
BorderPane brightnessPane = new BorderPane();
|
||||
themeList.getContent().add(brightnessPane);
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.brightness"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
brightnessPane.setLeft(left);
|
||||
|
||||
JFXComboBox<String> cboBrightness = new JFXComboBox<>(FXCollections.observableArrayList(
|
||||
FXUtils.DARK_MODE != null
|
||||
? List.of("auto", "light", "dark")
|
||||
: List.of("light", "dark")
|
||||
));
|
||||
cboBrightness.setConverter(FXUtils.stringConverter(name -> i18n("settings.launcher.brightness." + name)));
|
||||
cboBrightness.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
brightnessPane.setRight(cboBrightness);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -105,13 +116,13 @@ public class PersonalizationPage extends StackPane {
|
||||
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
|
||||
}
|
||||
{
|
||||
LineToggleButton titleTransparentButton = new LineToggleButton();
|
||||
OptionToggleButton titleTransparentButton = new OptionToggleButton();
|
||||
themeList.getContent().add(titleTransparentButton);
|
||||
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
|
||||
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
|
||||
}
|
||||
{
|
||||
LineToggleButton animationButton = new LineToggleButton();
|
||||
OptionToggleButton animationButton = new OptionToggleButton();
|
||||
themeList.getContent().add(animationButton);
|
||||
animationButton.selectedProperty().bindBidirectional(config().animationDisabledProperty());
|
||||
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
|
||||
@@ -180,7 +191,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
Label textOpacity = new Label();
|
||||
FXUtils.setLimitWidth(textOpacity, 50);
|
||||
FXUtils.setLimitWidth(textOpacity, 35);
|
||||
|
||||
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
|
||||
textOpacity.textProperty().bind(valueBinding);
|
||||
@@ -197,7 +208,8 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
ComponentList logPane = new ComponentList();
|
||||
ComponentList logPane = new ComponentSublist();
|
||||
logPane.setTitle(i18n("settings.launcher.log"));
|
||||
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
@@ -253,7 +265,8 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
ComponentList fontPane = new ComponentList();
|
||||
ComponentSublist fontPane = new ComponentSublist();
|
||||
fontPane.setTitle(i18n("settings.launcher.font"));
|
||||
|
||||
{
|
||||
VBox vbox = new VBox();
|
||||
@@ -295,32 +308,45 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
var fontAntiAliasingPane = new LineSelectButton<Optional<FontSmoothingType>>();
|
||||
fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing"));
|
||||
fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
fontAntiAliasingPane.setConverter(value ->
|
||||
value.isPresent()
|
||||
? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT))
|
||||
: i18n("settings.launcher.font.anti_aliasing.auto")
|
||||
);
|
||||
fontAntiAliasingPane.setItems(
|
||||
Optional.empty(),
|
||||
Optional.of(FontSmoothingType.LCD),
|
||||
Optional.of(FontSmoothingType.GRAY)
|
||||
);
|
||||
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.LCD));
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.GRAY));
|
||||
} else {
|
||||
fontAntiAliasingPane.setValue(Optional.empty());
|
||||
BorderPane fontAntiAliasingPane = new BorderPane();
|
||||
{
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.font.anti_aliasing"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
fontAntiAliasingPane.setLeft(left);
|
||||
}
|
||||
|
||||
FXUtils.onChange(fontAntiAliasingPane.valueProperty(), value ->
|
||||
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
|
||||
.orElse(null)));
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
JFXComboBox<Optional<FontSmoothingType>> cboAntiAliasing = new JFXComboBox<>(FXCollections.observableArrayList(
|
||||
Optional.empty(),
|
||||
Optional.of(FontSmoothingType.LCD),
|
||||
Optional.of(FontSmoothingType.GRAY)
|
||||
));
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.LCD));
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.GRAY));
|
||||
} else {
|
||||
cboAntiAliasing.setValue(Optional.empty());
|
||||
}
|
||||
cboAntiAliasing.setConverter(FXUtils.stringConverter(value -> {
|
||||
if (value.isPresent()) {
|
||||
return i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT));
|
||||
} else {
|
||||
return i18n("settings.launcher.font.anti_aliasing.auto");
|
||||
}
|
||||
}));
|
||||
FXUtils.onChange(cboAntiAliasing.valueProperty(), value ->
|
||||
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
|
||||
.orElse(null)));
|
||||
|
||||
fontAntiAliasingPane.setRight(cboAntiAliasing);
|
||||
}
|
||||
|
||||
fontPane.getContent().add(fontAntiAliasingPane);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -35,18 +38,17 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTFileType;
|
||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -65,7 +67,7 @@ import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -146,7 +148,12 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
// first item in left sidebar
|
||||
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
|
||||
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
|
||||
FXUtils.onSecondaryButtonClicked(accountListItem, () -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, accountListItem.getWidth(), 0));
|
||||
accountListItem.setOnMouseClicked(e -> {
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
showAccountListPopupMenu(accountListItem);
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
// second item in left sidebar
|
||||
@@ -167,7 +174,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareVersionPage);
|
||||
}
|
||||
FXUtils.onSecondaryButtonClicked(gameListItem, () -> showGameListPopupMenu(gameListItem));
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
@@ -175,7 +181,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
gameItem.setActionButtonVisible(false);
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
|
||||
|
||||
// forth item in left sidebar
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
@@ -249,14 +254,34 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
setCenter(getSkinnable().getMainPage());
|
||||
}
|
||||
|
||||
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
|
||||
GameListPopupMenu.show(gameListItem,
|
||||
JFXPopup.PopupVPosition.TOP,
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
gameListItem.getWidth(),
|
||||
0,
|
||||
getSkinnable().getMainPage().getProfile(),
|
||||
getSkinnable().getMainPage().getVersions());
|
||||
public void showAccountListPopupMenu(
|
||||
AccountAdvancedListItem accountListItem
|
||||
) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
AdvancedListBox scrollPane = new AdvancedListBox();
|
||||
scrollPane.getStyleClass().add("no-padding");
|
||||
scrollPane.setPrefWidth(220);
|
||||
scrollPane.setPrefHeight(-1);
|
||||
scrollPane.setMaxHeight(260);
|
||||
|
||||
if (Accounts.getAccounts().isEmpty()) {
|
||||
Label placeholder = new Label(i18n("account.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
scrollPane.add(placeholder);
|
||||
} else {
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
AccountAdvancedListItem item = new AccountAdvancedListItem(account);
|
||||
item.setOnAction(e -> {
|
||||
Accounts.setSelectedAccount(account);
|
||||
popup.hide();
|
||||
});
|
||||
scrollPane.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.getContent().add(scrollPane);
|
||||
popup.show(accountListItem, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, accountListItem.getWidth(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,21 +293,11 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
checkedModpack = true;
|
||||
|
||||
if (repository.getVersionCount() == 0) {
|
||||
Path zipModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
|
||||
Path mrpackModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.mrpack");
|
||||
|
||||
Path modpackFile;
|
||||
if (Files.exists(zipModpack)) {
|
||||
modpackFile = zipModpack;
|
||||
} else if (Files.exists(mrpackModpack)) {
|
||||
modpackFile = mrpackModpack;
|
||||
} else {
|
||||
modpackFile = null;
|
||||
}
|
||||
|
||||
if (modpackFile != null) {
|
||||
Path modpackFile = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
|
||||
if (Files.exists(modpackFile)) {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
|
||||
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
|
||||
.thenApplyAsync(
|
||||
encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
|
||||
.thenApplyAsync(modpack -> ModpackHelper
|
||||
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null)
|
||||
.executor())
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
@@ -39,8 +40,11 @@ import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
@@ -59,12 +63,15 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
@@ -201,7 +208,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton previewPane = new LineToggleButton();
|
||||
OptionToggleButton previewPane = new OptionToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.setSubtitle(i18n("update.preview.subtitle"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
@@ -245,27 +252,37 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
BorderPane languagePane = new BorderPane();
|
||||
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.language"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
languagePane.setLeft(left);
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
chooseLanguagePane.setConverter(locale -> {
|
||||
JFXComboBox<SupportedLocale> cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
|
||||
}));
|
||||
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
|
||||
settingsPane.getContent().add(chooseLanguagePane);
|
||||
FXUtils.setLimitWidth(cboLanguage, 300);
|
||||
languagePane.setRight(cboLanguage);
|
||||
|
||||
settingsPane.getContent().add(languagePane);
|
||||
}
|
||||
|
||||
{
|
||||
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
|
||||
OptionToggleButton disableAutoGameOptionsPane = new OptionToggleButton();
|
||||
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
|
||||
@@ -313,19 +330,6 @@ public final class SettingsPage extends ScrollPane {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
private static String getEntryName(Set<String> entryNames, String name) {
|
||||
if (entryNames.add(name)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
for (long i = 1; ; i++) {
|
||||
String newName = name + "." + i;
|
||||
if (entryNames.add(newName)) {
|
||||
return newName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method guarantees to close both `input` and the current zip entry.
|
||||
///
|
||||
/// If no exception occurs, this method returns `true`;
|
||||
@@ -385,8 +389,6 @@ public final class SettingsPage extends ScrollPane {
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
@@ -408,7 +410,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
input = null;
|
||||
}
|
||||
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
String entryName = StringUtils.substringBeforeLast(fileName, ".");
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
@@ -425,10 +427,10 @@ public final class SettingsPage extends ScrollPane {
|
||||
continue;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
exportLogFile(zos, path, fileName, input, buffer);
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
zos.putNextEntry(new ZipEntry("hmcl-latest.log"));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@ package org.jackhuang.hmcl.ui.nbt;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
@@ -37,22 +36,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NBTTreeView extends TreeView<Tag> {
|
||||
public final class NBTTreeView extends JFXTreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected javafx.scene.control.Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<Tag>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
|
||||
@@ -28,9 +28,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
|
||||
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
public ProfileListItemSkin(ProfileListItem skinnable) {
|
||||
super(skinnable);
|
||||
@@ -46,7 +47,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
|
||||
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
|
||||
|
||||
Node left = FXUtils.wrap(SVG.FOLDER);
|
||||
Node left = VersionPage.wrap(SVG.FOLDER);
|
||||
root.setLeft(left);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -55,7 +55,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
private final Profile profile;
|
||||
private final JFXTextField txtProfileName;
|
||||
private final FileItem gameDir;
|
||||
private final LineToggleButton toggleUseRelativePath;
|
||||
private final OptionToggleButton toggleUseRelativePath;
|
||||
|
||||
/**
|
||||
* @param profile null if creating a new profile.
|
||||
@@ -79,6 +79,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
rootPane.setStyle("-fx-padding: 20;");
|
||||
{
|
||||
ComponentList componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
{
|
||||
BorderPane profileNamePane = new BorderPane();
|
||||
{
|
||||
@@ -112,7 +113,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
gameDir.setTitle(i18n("profile.instance_directory.choose"));
|
||||
gameDir.pathProperty().bindBidirectional(location);
|
||||
|
||||
toggleUseRelativePath = new LineToggleButton();
|
||||
toggleUseRelativePath = new OptionToggleButton();
|
||||
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
|
||||
|
||||
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
|
||||
|
||||
@@ -245,19 +245,19 @@ public class TerracottaControllerPage extends StackPane {
|
||||
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(guest, () -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, resolve, reject) -> {
|
||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
||||
if (e != null) {
|
||||
handler.reject(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||
reject.accept(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||
} else {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
} else {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -455,7 +455,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||
exportLog.setContent(exportLogInner);
|
||||
ComponentList.setNoPadding(exportLog);
|
||||
exportLog.getProperties().put("ComponentList.noPadding", true);
|
||||
// FIXME: SpinnerPane loses its content width in loading state.
|
||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||
|
||||
@@ -559,7 +559,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
private ComponentList getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
locals.setComponentPadding(false);
|
||||
|
||||
@@ -587,7 +587,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||
() -> FXUtils.openLink(url)
|
||||
));
|
||||
ComponentList.setNoPadding(container);
|
||||
container.getProperties().put("ComponentList.noPadding", true);
|
||||
locals.getContent().add(container);
|
||||
}
|
||||
return locals;
|
||||
@@ -666,7 +666,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
ComponentList.setNoPadding(button);
|
||||
button.getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.terracotta;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
@@ -25,21 +24,17 @@ import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.main.MainPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -72,13 +67,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
.addNavigationDrawerTab(tab, statusPage, i18n("terracotta.status"), SVG.TUNE);
|
||||
left.setTop(sideBar);
|
||||
|
||||
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
|
||||
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
FXUtils.onSecondaryButtonClicked(accountListItem, () -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, accountListItem.getWidth(), 0));
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox()
|
||||
.add(accountListItem)
|
||||
.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), launcherHelper -> {
|
||||
@@ -95,13 +84,6 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
String currentId = mainPage.getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> mainPage.getProfile().setSelectedVersion(it.getId()));
|
||||
|
||||
FXUtils.onSecondaryButtonClicked(item, () -> GameListPopupMenu.show(item,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
item.getWidth(),
|
||||
0,
|
||||
mainPage.getProfile(), mainPage.getVersions()));
|
||||
})
|
||||
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -24,6 +26,7 @@ import java.nio.file.FileSystems;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class AdvancedVersionSettingPage extends StackPane implements DecoratorPage {
|
||||
@@ -41,17 +44,17 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final JFXTextField txtWrapper;
|
||||
private final JFXTextField txtPreLaunchCommand;
|
||||
private final JFXTextField txtPostExitCommand;
|
||||
private final LineToggleButton noJVMArgsPane;
|
||||
private final LineToggleButton noOptimizingJVMArgsPane;
|
||||
private final LineToggleButton noGameCheckPane;
|
||||
private final LineToggleButton noJVMCheckPane;
|
||||
private final LineToggleButton noNativesPatchPane;
|
||||
private final LineToggleButton useNativeGLFWPane;
|
||||
private final LineToggleButton useNativeOpenALPane;
|
||||
private final OptionToggleButton noJVMArgsPane;
|
||||
private final OptionToggleButton noOptimizingJVMArgsPane;
|
||||
private final OptionToggleButton noGameCheckPane;
|
||||
private final OptionToggleButton noJVMCheckPane;
|
||||
private final OptionToggleButton noNativesPatchPane;
|
||||
private final OptionToggleButton useNativeGLFWPane;
|
||||
private final OptionToggleButton useNativeOpenALPane;
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final LineSelectButton<Renderer> rendererPane;
|
||||
private final JFXComboBox<Renderer> cboRenderer;
|
||||
|
||||
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
|
||||
this.profile = profile;
|
||||
@@ -167,31 +170,40 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
|
||||
nativesDirItem.getChildren().add(nativesDirHint);
|
||||
|
||||
rendererPane = new LineSelectButton<>();
|
||||
rendererPane.setTitle(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
|
||||
rendererPane.setItems(Renderer.values());
|
||||
BorderPane rendererPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
noJVMArgsPane = new LineToggleButton();
|
||||
cboRenderer = new JFXComboBox<>();
|
||||
cboRenderer.getItems().setAll(Renderer.values());
|
||||
cboRenderer.setConverter(stringConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT))));
|
||||
rendererPane.setRight(cboRenderer);
|
||||
BorderPane.setAlignment(cboRenderer, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboRenderer, 300);
|
||||
}
|
||||
|
||||
noJVMArgsPane = new OptionToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
|
||||
noOptimizingJVMArgsPane = new LineToggleButton();
|
||||
noOptimizingJVMArgsPane = new OptionToggleButton();
|
||||
noOptimizingJVMArgsPane.setTitle(i18n("settings.advanced.no_optimizing_jvm_args"));
|
||||
noOptimizingJVMArgsPane.disableProperty().bind(noJVMArgsPane.selectedProperty());
|
||||
|
||||
noGameCheckPane = new LineToggleButton();
|
||||
noGameCheckPane = new OptionToggleButton();
|
||||
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
|
||||
|
||||
noJVMCheckPane = new LineToggleButton();
|
||||
noJVMCheckPane = new OptionToggleButton();
|
||||
noJVMCheckPane.setTitle(i18n("settings.advanced.dont_check_jvm_validity"));
|
||||
|
||||
noNativesPatchPane = new LineToggleButton();
|
||||
noNativesPatchPane = new OptionToggleButton();
|
||||
noNativesPatchPane.setTitle(i18n("settings.advanced.dont_patch_natives"));
|
||||
|
||||
useNativeGLFWPane = new LineToggleButton();
|
||||
useNativeGLFWPane = new OptionToggleButton();
|
||||
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
|
||||
|
||||
useNativeOpenALPane = new LineToggleButton();
|
||||
useNativeOpenALPane = new OptionToggleButton();
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
@@ -227,7 +239,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
|
||||
FXUtils.bindEnum(cboRenderer, versionSetting.rendererProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
@@ -269,7 +281,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.unbind(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
|
||||
FXUtils.unbindEnum(cboRenderer, versionSetting.rendererProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -43,29 +42,20 @@ import java.util.regex.Pattern;
|
||||
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 {
|
||||
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
|
||||
private final Path worldDir;
|
||||
private final Datapack datapack;
|
||||
final BooleanProperty readOnly;
|
||||
|
||||
public DatapackListPage(WorldManagePage worldManagePage) {
|
||||
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)),
|
||||
this::installMultiDatapack, this::refresh);
|
||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void installMultiDatapack(List<Path> datapackPath) {
|
||||
datapackPath.forEach(this::installSingleDatapack);
|
||||
if (readOnly.get()) {
|
||||
Controllers.showToast(i18n("datapack.reload.toast"));
|
||||
}
|
||||
}
|
||||
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
try {
|
||||
this.datapack.installPack(datapack);
|
||||
@@ -93,7 +83,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null) {
|
||||
installMultiDatapack(res);
|
||||
res.forEach(this::installSingleDatapack);
|
||||
}
|
||||
|
||||
datapack.loadFromDir();
|
||||
|
||||
@@ -114,23 +114,16 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
||||
);
|
||||
|
||||
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
});
|
||||
JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||
JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||
removeButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
enableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
disableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
|
||||
selectingToolbar.getChildren().addAll(
|
||||
removeButton,
|
||||
enableButton,
|
||||
disableButton,
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
@@ -186,7 +179,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
|
||||
@@ -309,7 +302,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
final TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView) {
|
||||
super(listView);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
@@ -319,8 +312,6 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
checkBox.disableProperty().bind(isReadOnlyProperty);
|
||||
|
||||
imageView.setFitWidth(32);
|
||||
imageView.setFitHeight(32);
|
||||
imageView.setPreserveRatio(true);
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.*;
|
||||
@@ -28,9 +30,11 @@ import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -48,7 +52,7 @@ 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.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.FloatListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -521,65 +525,46 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
||||
listView.setSelectionModel(null);
|
||||
FXUtils.onClicked(listView, () -> {
|
||||
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
RemoteMod selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
Controllers.navigate(new DownloadPage(getSkinnable(), selectedItem, getSkinnable().getProfileVersion(), getSkinnable().callback));
|
||||
});
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
private static final Insets PADDING = new Insets(9, 9, 0, 9);
|
||||
|
||||
private final RipplerContainer graphic;
|
||||
|
||||
listView.setCellFactory(x -> new FloatListCell<>(listView) {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
|
||||
{
|
||||
setPadding(PADDING);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.getStyleClass().add("card");
|
||||
container.setCursor(Cursor.HAND);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
JFXDepthManager.setDepth(container, 1);
|
||||
pane.getChildren().add(container);
|
||||
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
|
||||
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
this.graphic = new RipplerContainer(container);
|
||||
graphic.setPosition(JFXRippler.RipplerPos.FRONT);
|
||||
FXUtils.onClicked(graphic, () -> {
|
||||
RemoteMod item = getItem();
|
||||
if (item != null)
|
||||
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getProfileVersion(), getSkinnable().callback));
|
||||
});
|
||||
|
||||
setPrefWidth(0);
|
||||
|
||||
FXUtils.limitCellWidth(listView, this);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(RemoteMod item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.getTitle());
|
||||
content.setSubtitle(item.getDescription());
|
||||
content.getTags().clear();
|
||||
for (String category : item.getCategories()) {
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
content.addTag(getSkinnable().getLocalizedCategory(category));
|
||||
}
|
||||
iconLoader.load(imageView.imageProperty(), item.getIconUrl());
|
||||
setGraphic(graphic);
|
||||
protected void updateControl(RemoteMod dataItem, boolean empty) {
|
||||
if (empty) return;
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(dataItem.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getDescription());
|
||||
content.getTags().clear();
|
||||
for (String category : dataItem.getCategories()) {
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
content.addTag(getSkinnable().getLocalizedCategory(category));
|
||||
}
|
||||
iconLoader.load(imageView.imageProperty(), dataItem.getIconUrl());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,6 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
ModTranslations.Mod mod = getSkinnable().translations.getModByCurseForgeId(getSkinnable().addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getSubtitleLabel().setWrapText(true);
|
||||
getSkinnable().addon.getCategories().stream()
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
@@ -300,13 +299,13 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.toList()) {
|
||||
.collect(Collectors.toList())) {
|
||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var sublist = new ComponentSublist(() -> {
|
||||
ComponentList sublist = new ComponentList(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
@@ -465,14 +464,13 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
box.getChildren().setAll(modItem);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList dependenciesList = new ComponentList();
|
||||
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
|
||||
@@ -70,7 +70,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
addProfileItem.getStyleClass().add("navigation-drawer-item");
|
||||
addProfileItem.setTitle(i18n("profile.new"));
|
||||
addProfileItem.setActionButtonVisible(false);
|
||||
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
|
||||
addProfileItem.setLeftGraphic(VersionPage.wrap(SVG.ADD_CIRCLE));
|
||||
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
|
||||
|
||||
pane.setFitToWidth(true);
|
||||
|
||||
@@ -26,7 +26,6 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
@@ -35,29 +34,15 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
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.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// @author Glavo
|
||||
public final class GameListPopupMenu extends StackPane {
|
||||
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
Profile profile, List<Version> versions) {
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
}
|
||||
|
||||
private final JFXListView<GameItem> listView = new JFXListView<>();
|
||||
private final BooleanBinding isEmpty = Bindings.isEmpty(listView.getItems());
|
||||
|
||||
|
||||
@@ -76,12 +76,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion, InstallerItem.Style.LIST_ITEM);
|
||||
|
||||
// Conventional libraries: game, fabric, legacyfabric, forge, cleanroom, neoforge, liteloader, optifine
|
||||
// Conventional libraries: game, fabric, forge, cleanroom, neoforge, liteloader, optifine
|
||||
for (InstallerItem item : group.getLibraries()) {
|
||||
String libraryId = item.getLibraryId();
|
||||
|
||||
// Skip fabric-api and quilt-api and legacyfabric-api
|
||||
if (libraryId.endsWith("-api")) {
|
||||
// Skip fabric-api and quilt-api
|
||||
if (libraryId.contains("fabric-api") || libraryId.contains("quilt-api")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,45 +19,34 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
|
||||
private final List<Task<LocalModFile.ModUpdate>> dependents;
|
||||
private final String gameVersion;
|
||||
private final Collection<LocalModFile> mods;
|
||||
private final Collection<Collection<Task<LocalModFile.ModUpdate>>> dependents;
|
||||
|
||||
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(gameVersion, type.getRemoteModRepository());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
|
||||
}
|
||||
if (update == null) {
|
||||
continue;
|
||||
}
|
||||
this.gameVersion = gameVersion;
|
||||
this.mods = mods;
|
||||
|
||||
if (candidate == null || candidate.getCandidate().getDatePublished().isBefore(update.getCandidate().getDatePublished())) {
|
||||
candidate = update;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}).setName(mod.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
|
||||
).toList();
|
||||
dependents = mods.stream()
|
||||
.map(mod ->
|
||||
Arrays.stream(RemoteMod.Type.values())
|
||||
.map(type ->
|
||||
Task.supplyAsync(() -> mod.checkUpdates(gameVersion, type.getRemoteModRepository()))
|
||||
.setSignificance(TaskSignificance.MAJOR)
|
||||
.setName(String.format("%s (%s)", mod.getFileName(), type.name())).withCounter("update.checking")
|
||||
)
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
setStage("update.checking");
|
||||
getProperties().put("total", dependents.size());
|
||||
getProperties().put("total", dependents.size() * RemoteMod.Type.values().length);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,7 +61,7 @@ public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return dependents;
|
||||
return dependents.stream().flatMap(Collection::stream).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,7 +72,14 @@ public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(dependents.stream()
|
||||
.map(Task::getResult)
|
||||
.filter(Objects::nonNull).toList());
|
||||
.map(tasks -> tasks.stream()
|
||||
.filter(task -> task.getResult() != null)
|
||||
.map(Task::getResult)
|
||||
.filter(modUpdate -> !modUpdate.getCandidates().isEmpty())
|
||||
.max(Comparator.comparing((LocalModFile.ModUpdate modUpdate) -> modUpdate.getCandidates().get(0).getDatePublished()))
|
||||
.orElse(null)
|
||||
)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
@@ -152,10 +152,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGED);
|
||||
|
||||
@@ -470,18 +470,29 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGED -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
default -> null;
|
||||
};
|
||||
if (loaderName == null)
|
||||
continue;
|
||||
String loaderName;
|
||||
switch (modLoaderType) {
|
||||
case FORGE:
|
||||
loaderName = i18n("install.installer.forge");
|
||||
break;
|
||||
case CLEANROOM:
|
||||
loaderName = i18n("install.installer.cleanroom");
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
loaderName = i18n("install.installer.neoforge");
|
||||
break;
|
||||
case FABRIC:
|
||||
loaderName = i18n("install.installer.fabric");
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
loaderName = i18n("install.installer.liteloader");
|
||||
break;
|
||||
case QUILT:
|
||||
loaderName = i18n("install.installer.quilt");
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
@@ -653,13 +664,24 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGED -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
case FORGE:
|
||||
content.addTagWarning(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTagWarning(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTagWarning(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
modManager,
|
||||
objects.stream()
|
||||
.filter(o -> o.enabled.get())
|
||||
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidate()))
|
||||
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0)))
|
||||
.collect(Collectors.toList()));
|
||||
Controllers.taskDialog(
|
||||
task.whenComplete(Schedulers.javafx(), exception -> {
|
||||
@@ -203,7 +203,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
enabled.set(!data.getLocalMod().getModManager().isDisabled(data.getLocalMod().getFile()));
|
||||
fileName.set(data.getLocalMod().getFileName());
|
||||
currentVersion.set(data.getCurrentVersion().getVersion());
|
||||
targetVersion.set(data.getCandidate().getVersion());
|
||||
targetVersion.set(data.getCandidates().get(0).getVersion());
|
||||
switch (data.getCurrentVersion().getSelf().getType()) {
|
||||
case CURSEFORGE:
|
||||
source.set(i18n("mods.curseforge"));
|
||||
|
||||
@@ -9,8 +9,6 @@ import javafx.scene.control.Skin;
|
||||
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.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -142,8 +140,6 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
|
||||
@@ -19,10 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -32,7 +30,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -163,30 +160,30 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
i18n("schematics.create_directory.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
(result, resolve, reject) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
|
||||
reject.accept(i18n("schematics.create_directory.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
reject.accept(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = parent.resolve(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||
reject.accept(i18n("schematics.create_directory.failed.already_exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(targetDir);
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
refresh();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||
reject.accept(i18n("schematics.create_directory.failed", targetDir));
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -224,7 +221,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
getItems().addAll(item.children);
|
||||
}
|
||||
|
||||
abstract sealed class Item implements Comparable<Item> {
|
||||
abstract class Item extends Control implements Comparable<Item> {
|
||||
|
||||
boolean isDirectory() {
|
||||
return this instanceof DirItem;
|
||||
@@ -261,6 +258,11 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
|
||||
return this.getName().compareTo(o.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ItemSkin(this);
|
||||
}
|
||||
}
|
||||
|
||||
private final class BackItem extends Item {
|
||||
@@ -436,10 +438,6 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return SVG.SCHEMA;
|
||||
}
|
||||
|
||||
public @Nullable Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
Node getIcon(int size) {
|
||||
if (image == null) {
|
||||
return super.getIcon(size);
|
||||
@@ -542,122 +540,62 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Cell extends ListCell<Item> {
|
||||
private static final class ItemSkin extends SkinBase<Item> {
|
||||
public ItemSkin(Item item) {
|
||||
super(item);
|
||||
|
||||
private final RipplerContainer graphics;
|
||||
private final BorderPane root;
|
||||
private final StackPane left;
|
||||
private final TwoLineListItem center;
|
||||
private final HBox right;
|
||||
|
||||
private final ImageView iconImageView;
|
||||
private final SVGPath iconSVG;
|
||||
private final StackPane iconSVGWrapper;
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
public Cell() {
|
||||
this.root = new BorderPane();
|
||||
BorderPane root = new BorderPane();
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
{
|
||||
this.left = new StackPane();
|
||||
StackPane left = new StackPane();
|
||||
left.setMaxSize(32, 32);
|
||||
left.setPrefSize(32, 32);
|
||||
left.getChildren().add(item.getIcon(24));
|
||||
left.setPadding(new Insets(0, 8, 0, 0));
|
||||
|
||||
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);
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
FXUtils.installSlowTooltip(left, path.toAbsolutePath().normalize().toString());
|
||||
}
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER);
|
||||
root.setLeft(left);
|
||||
}
|
||||
|
||||
{
|
||||
this.center = new TwoLineListItem();
|
||||
TwoLineListItem center = new TwoLineListItem();
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
|
||||
root.setCenter(center);
|
||||
}
|
||||
|
||||
{
|
||||
this.right = new HBox(8);
|
||||
if (!(item instanceof BackItem)) {
|
||||
HBox right = new HBox(8);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
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();
|
||||
});
|
||||
btnReveal.setOnAction(event -> item.onReveal());
|
||||
|
||||
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)) {
|
||||
btnDelete.setOnAction(event ->
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
|
||||
item::onDelete, null);
|
||||
}
|
||||
});
|
||||
item::onDelete, null));
|
||||
|
||||
right.getChildren().setAll(btnReveal, btnDelete);
|
||||
root.setRight(right);
|
||||
}
|
||||
|
||||
this.graphics = new RipplerContainer(root);
|
||||
FXUtils.onClicked(graphics, () -> {
|
||||
Item item = getItem();
|
||||
if (item != null)
|
||||
item.onClick();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Item item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
iconImageView.setImage(null);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
center.setTitle("");
|
||||
center.setSubtitle("");
|
||||
} else {
|
||||
if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) {
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVG.setContent(item.getIcon().getPath());
|
||||
left.getChildren().setAll(iconSVGWrapper);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
tooltip.setText(FileUtils.getAbsolutePath(path));
|
||||
FXUtils.installSlowTooltip(left, tooltip);
|
||||
} else {
|
||||
tooltip.setText("");
|
||||
Tooltip.uninstall(left, tooltip);
|
||||
}
|
||||
|
||||
root.setRight(item instanceof BackItem ? null : right);
|
||||
|
||||
setGraphic(graphics);
|
||||
}
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
FXUtils.onClicked(container, item::onClick);
|
||||
this.getChildren().add(container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,10 +612,5 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListCell<Item> createListCell(JFXListView<Item> listView) {
|
||||
return new Cell();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ public class VersionIconDialog extends DialogPane {
|
||||
createIcon(VersionIconType.OPTIFINE),
|
||||
createIcon(VersionIconType.CRAFT_TABLE),
|
||||
createIcon(VersionIconType.FABRIC),
|
||||
createIcon(VersionIconType.LEGACY_FABRIC),
|
||||
createIcon(VersionIconType.FORGE),
|
||||
createIcon(VersionIconType.CLEANROOM),
|
||||
createIcon(VersionIconType.NEO_FORGE),
|
||||
|
||||
@@ -21,8 +21,11 @@ import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
@@ -302,6 +305,20 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(stackPane, 30);
|
||||
FXUtils.setLimitHeight(stackPane, 20);
|
||||
stackPane.setPadding(new Insets(0, 0, 0, 0));
|
||||
stackPane.getChildren().setAll(node);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public static Node wrap(SVG svg) {
|
||||
return wrap(svg.createIcon(20));
|
||||
}
|
||||
|
||||
public interface VersionLoadable {
|
||||
void loadVersion(Profile profile, String version);
|
||||
}
|
||||
|
||||
@@ -42,8 +42,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.ServerAddress;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
@@ -54,17 +56,34 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.getTimer;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage, VersionPage.VersionLoadable, PageAware {
|
||||
|
||||
private static final ObjectProperty<PhysicalMemoryStatus> memoryStatus = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
private static TimerTask memoryStatusUpdateTask;
|
||||
|
||||
private static void initMemoryStatusUpdateTask() {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (memoryStatusUpdateTask != null)
|
||||
return;
|
||||
memoryStatusUpdateTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Platform.runLater(() -> memoryStatus.set(SystemInfo.getPhysicalMemoryStatus()));
|
||||
}
|
||||
};
|
||||
getTimer().scheduleAtFixedRate(memoryStatusUpdateTask, 0, 1000);
|
||||
}
|
||||
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(new State("", null, false, false, false));
|
||||
|
||||
private AdvancedVersionSettingPage advancedVersionSettingPage;
|
||||
@@ -78,7 +97,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final JFXComboBox<String> cboWindowsSize;
|
||||
private final JFXTextField txtServerIP;
|
||||
private final ComponentList componentList;
|
||||
private final LineSelectButton<LauncherVisibility> launcherVisibilityPane;
|
||||
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
private final JFXCheckBox chkAutoAllocate;
|
||||
private final JFXCheckBox chkFullscreen;
|
||||
private final ComponentSublist javaSublist;
|
||||
@@ -90,9 +109,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final ComponentSublist gameDirSublist;
|
||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
|
||||
private final LineSelectButton<ProcessPriority> processPriorityPane;
|
||||
private final LineToggleButton showLogsPane;
|
||||
private final LineToggleButton enableDebugLogOutputPane;
|
||||
private final JFXComboBox<ProcessPriority> cboProcessPriority;
|
||||
private final OptionToggleButton showLogsPane;
|
||||
private final OptionToggleButton enableDebugLogOutputPane;
|
||||
private final ImagePickerItem iconPickerItem;
|
||||
|
||||
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
|
||||
@@ -108,13 +127,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
|
||||
private final BooleanProperty modpack = new SimpleBooleanProperty();
|
||||
|
||||
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
|
||||
|
||||
public VersionSettingsPage(boolean globalSetting) {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
rootPane = new VBox();
|
||||
@@ -173,6 +190,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
{
|
||||
componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
|
||||
if (!globalSetting) {
|
||||
BorderPane copyGlobalPane = new BorderPane();
|
||||
@@ -350,10 +368,17 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
|
||||
}
|
||||
|
||||
launcherVisibilityPane = new LineSelectButton<>();
|
||||
launcherVisibilityPane.setTitle(i18n("settings.advanced.launcher_visible"));
|
||||
launcherVisibilityPane.setItems(LauncherVisibility.values());
|
||||
launcherVisibilityPane.setConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
|
||||
BorderPane launcherVisibilityPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.launcher_visible"));
|
||||
launcherVisibilityPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
cboLauncherVisibility = new JFXComboBox<>();
|
||||
launcherVisibilityPane.setRight(cboLauncherVisibility);
|
||||
BorderPane.setAlignment(cboLauncherVisibility, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboLauncherVisibility, 300);
|
||||
}
|
||||
|
||||
BorderPane dimensionPane = new BorderPane();
|
||||
{
|
||||
@@ -383,19 +408,23 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
}
|
||||
}
|
||||
|
||||
showLogsPane = new LineToggleButton();
|
||||
showLogsPane = new OptionToggleButton();
|
||||
showLogsPane.setTitle(i18n("settings.show_log"));
|
||||
|
||||
enableDebugLogOutputPane = new LineToggleButton();
|
||||
enableDebugLogOutputPane = new OptionToggleButton();
|
||||
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
|
||||
processPriorityPane = new LineSelectButton<>();
|
||||
processPriorityPane.setTitle(i18n("settings.advanced.process_priority"));
|
||||
processPriorityPane.setConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
|
||||
processPriorityPane.setDescriptionConverter(e -> {
|
||||
String bundleKey = "settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
});
|
||||
processPriorityPane.setItems(ProcessPriority.values());
|
||||
|
||||
BorderPane processPriorityPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.process_priority"));
|
||||
processPriorityPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
cboProcessPriority = new JFXComboBox<>();
|
||||
processPriorityPane.setRight(cboProcessPriority);
|
||||
BorderPane.setAlignment(cboProcessPriority, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboProcessPriority, 300);
|
||||
}
|
||||
|
||||
GridPane serverPane = new GridPane();
|
||||
{
|
||||
@@ -424,16 +453,23 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
|
||||
LineNavigationButton showAdvancedSettingPane = new LineNavigationButton();
|
||||
showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
|
||||
showAdvancedSettingPane.setOnAction(event -> {
|
||||
if (lastVersionSetting != null) {
|
||||
if (advancedVersionSettingPage == null)
|
||||
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
|
||||
BorderPane showAdvancedSettingPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced"));
|
||||
showAdvancedSettingPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
Controllers.navigateForward(advancedVersionSettingPage);
|
||||
}
|
||||
});
|
||||
JFXButton button = FXUtils.newBorderButton(i18n("settings.advanced.modify"));
|
||||
button.setOnAction(e -> {
|
||||
if (lastVersionSetting != null) {
|
||||
if (advancedVersionSettingPage == null)
|
||||
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
|
||||
|
||||
Controllers.navigateForward(advancedVersionSettingPage);
|
||||
}
|
||||
});
|
||||
showAdvancedSettingPane.setRight(button);
|
||||
}
|
||||
|
||||
componentList.getContent().addAll(
|
||||
javaSublist,
|
||||
@@ -468,7 +504,16 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
|
||||
|
||||
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
|
||||
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT))));
|
||||
|
||||
cboProcessPriority.getItems().setAll(ProcessPriority.values());
|
||||
cboProcessPriority.setConverter(stringConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT))));
|
||||
|
||||
memoryStatus.set(SystemInfo.getPhysicalMemoryStatus());
|
||||
componentList.disableProperty().bind(enableSpecificSettings.not());
|
||||
|
||||
initMemoryStatusUpdateTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -512,8 +557,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.selectedProperty().unbindBidirectional(lastVersionSetting.fullscreenProperty());
|
||||
showLogsPane.selectedProperty().unbindBidirectional(lastVersionSetting.showLogsProperty());
|
||||
enableDebugLogOutputPane.selectedProperty().unbindBidirectional(lastVersionSetting.enableDebugLogOutputProperty());
|
||||
launcherVisibilityPane.valueProperty().unbindBidirectional(lastVersionSetting.launcherVisibilityProperty());
|
||||
processPriorityPane.valueProperty().unbindBidirectional(lastVersionSetting.processPriorityProperty());
|
||||
FXUtils.unbindEnum(cboLauncherVisibility, lastVersionSetting.launcherVisibilityProperty());
|
||||
FXUtils.unbindEnum(cboProcessPriority, lastVersionSetting.processPriorityProperty());
|
||||
|
||||
lastVersionSetting.usesGlobalProperty().removeListener(usesGlobalListener);
|
||||
lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener);
|
||||
@@ -547,8 +592,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.selectedProperty().bindBidirectional(versionSetting.fullscreenProperty());
|
||||
showLogsPane.selectedProperty().bindBidirectional(versionSetting.showLogsProperty());
|
||||
enableDebugLogOutputPane.selectedProperty().bindBidirectional(versionSetting.enableDebugLogOutputProperty());
|
||||
launcherVisibilityPane.valueProperty().bindBidirectional(versionSetting.launcherVisibilityProperty());
|
||||
processPriorityPane.valueProperty().bindBidirectional(versionSetting.processPriorityProperty());
|
||||
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
|
||||
FXUtils.bindEnum(cboProcessPriority, versionSetting.processPriorityProperty());
|
||||
|
||||
if (versionId != null)
|
||||
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
|
||||
@@ -764,58 +809,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static final class UpdateMemoryStatus extends Thread {
|
||||
|
||||
@FXThread
|
||||
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
|
||||
|
||||
@FXThread
|
||||
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
if (memoryStatusPropertyCache != null) {
|
||||
var property = memoryStatusPropertyCache.get();
|
||||
if (property != null) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
memoryStatusPropertyCache = new WeakReference<>(property);
|
||||
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
|
||||
return property;
|
||||
}
|
||||
|
||||
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
|
||||
|
||||
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
|
||||
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
|
||||
|
||||
setName("UpdateMemoryStatus");
|
||||
setDaemon(true);
|
||||
setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
|
||||
|
||||
var memoryStatusProperty = memoryStatusPropertyRef.get();
|
||||
if (memoryStatusProperty == null)
|
||||
return;
|
||||
|
||||
if (Controllers.isStopped())
|
||||
return;
|
||||
|
||||
Platform.runLater(() -> memoryStatusProperty.set(status));
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,13 +129,13 @@ public final class Versions {
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
|
||||
if (newName.equals(version)) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
return;
|
||||
}
|
||||
if (profile.getRepository().renameVersion(version, newName)) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
profile.getRepository().refreshVersionsAsync()
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
if (profile.getRepository().hasVersion(newName)) {
|
||||
@@ -143,7 +143,7 @@ public final class Versions {
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
handler.reject(i18n("version.manage.rename.fail"));
|
||||
reject.accept(i18n("version.manage.rename.fail"));
|
||||
}
|
||||
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||
@@ -159,16 +159,16 @@ public final class Versions {
|
||||
|
||||
public static void duplicateVersion(Profile profile, String version) {
|
||||
Controllers.prompt(
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
|
||||
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
||||
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
} else {
|
||||
handler.reject(StringUtils.getStackTrace(exception));
|
||||
reject.accept(StringUtils.getStackTrace(exception));
|
||||
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
|
||||
profile.getRepository().removeVersionFromDisk(newVersionName);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -62,18 +61,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements WorldManagePage.WorldRefreshable {
|
||||
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
|
||||
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
|
||||
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final BooleanProperty readOnly;
|
||||
private final boolean isReadOnly;
|
||||
private final Pattern backupFileNamePattern;
|
||||
|
||||
public WorldBackupsPage(WorldManagePage worldManagePage) {
|
||||
this.world = worldManagePage.getWorld();
|
||||
this.backupsDir = worldManagePage.getBackupsDir();
|
||||
this.readOnly = worldManagePage.readOnlyProperty();
|
||||
this.isReadOnly = worldManagePage.isReadOnly();
|
||||
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
|
||||
|
||||
refresh();
|
||||
@@ -165,7 +164,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
|
||||
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
|
||||
createBackup.disableProperty().bind(getSkinnable().readOnly);
|
||||
createBackup.setDisable(isReadOnly);
|
||||
|
||||
return Arrays.asList(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -36,6 +36,7 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -43,19 +44,17 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -64,9 +63,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable {
|
||||
public final class WorldInfoPage extends SpinnerPane {
|
||||
private final WorldManagePage worldManagePage;
|
||||
private boolean isReadOnly;
|
||||
private final World world;
|
||||
private CompoundTag levelDat;
|
||||
|
||||
@@ -75,7 +73,19 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
public WorldInfoPage(WorldManagePage worldManagePage) {
|
||||
this.worldManagePage = worldManagePage;
|
||||
this.world = worldManagePage.getWorld();
|
||||
refresh();
|
||||
|
||||
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 CompoundTag loadWorldInfo() throws IOException {
|
||||
@@ -87,6 +97,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
private void updateControls() {
|
||||
CompoundTag dataTag = levelDat.get("Data");
|
||||
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -100,23 +111,22 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
rootPane.getStyleClass().add("card-list");
|
||||
|
||||
ComponentList worldInfo = new ComponentList();
|
||||
ComponentList basicInfo = new ComponentList();
|
||||
{
|
||||
var worldNamePane = new LinePane();
|
||||
BorderPane worldNamePane = new BorderPane();
|
||||
{
|
||||
worldNamePane.setTitle(i18n("world.name"));
|
||||
setLeftLabel(worldNamePane, "world.name");
|
||||
JFXTextField worldNameField = new JFXTextField();
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
|
||||
var worldName = new SimpleStringProperty(worldNameTag.getValue());
|
||||
FXUtils.bindString(worldNameField, worldName);
|
||||
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
|
||||
worldName.addListener((observable, oldValue, newValue) -> {
|
||||
if (StringUtils.isNotBlank(newValue)) {
|
||||
Tag tag = dataTag.get("LevelName");
|
||||
if (tag instanceof StringTag stringTag) {
|
||||
worldNameField.setText(stringTag.getValue());
|
||||
|
||||
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
world.setWorldName(newValue);
|
||||
worldManagePage.setTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to set world name", e);
|
||||
}
|
||||
@@ -127,18 +137,25 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
}
|
||||
|
||||
var gameVersionPane = new LineTextPane();
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
{
|
||||
gameVersionPane.setTitle(i18n("world.info.game_version"));
|
||||
gameVersionPane.setText(world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
setLeftLabel(gameVersionPane, "world.info.game_version");
|
||||
Label gameVersionLabel = new Label();
|
||||
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
}
|
||||
|
||||
var iconPane = new LinePane();
|
||||
BorderPane iconPane = new BorderPane();
|
||||
{
|
||||
iconPane.setTitle(i18n("world.icon"));
|
||||
setLeftLabel(iconPane, "world.icon");
|
||||
|
||||
Runnable onClickAction = () -> Controllers.confirm(
|
||||
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
);
|
||||
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
{
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
}
|
||||
|
||||
@@ -146,20 +163,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
JFXButton resetIconButton = new JFXButton();
|
||||
{
|
||||
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
||||
editIconButton.setDisable(isReadOnly);
|
||||
editIconButton.setOnAction(event -> Controllers.confirm(
|
||||
I18n.i18n("world.icon.change.tip"),
|
||||
I18n.i18n("world.icon.change"),
|
||||
MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
));
|
||||
editIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(editIconButton, onClickAction);
|
||||
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());
|
||||
resetIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
|
||||
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
||||
resetIconButton.getStyleClass().add("toggle-icon4");
|
||||
}
|
||||
@@ -171,15 +182,17 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
iconPane.setRight(hBox);
|
||||
}
|
||||
|
||||
var seedPane = new LinePane();
|
||||
BorderPane seedPane = new BorderPane();
|
||||
{
|
||||
seedPane.setTitle(i18n("world.info.random_seed"));
|
||||
setLeftLabel(seedPane, "world.info.random_seed");
|
||||
|
||||
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||
StackPane visibilityButton = new StackPane();
|
||||
{
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
}
|
||||
|
||||
@@ -201,235 +214,282 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
HBox right = new HBox(8);
|
||||
{
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.getChildren().setAll(visibilityButton, seedLabel);
|
||||
seedPane.setRight(right);
|
||||
}
|
||||
}
|
||||
|
||||
var worldSpawnPoint = new LineTextPane();
|
||||
BorderPane lastPlayedPane = new BorderPane();
|
||||
{
|
||||
worldSpawnPoint.setTitle(i18n("world.info.spawn"));
|
||||
|
||||
String value;
|
||||
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
|
||||
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());
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
|
||||
worldSpawnPoint.setText(value);
|
||||
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
||||
Label lastPlayedLabel = new Label();
|
||||
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
}
|
||||
|
||||
var lastPlayedPane = new LineTextPane();
|
||||
BorderPane timePane = new BorderPane();
|
||||
{
|
||||
lastPlayedPane.setTitle(i18n("world.info.last_played"));
|
||||
lastPlayedPane.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
setLeftLabel(timePane, "world.info.time");
|
||||
|
||||
Label timeLabel = new Label();
|
||||
setRightTextLabel(timePane, timeLabel, () -> {
|
||||
Tag tag = dataTag.get("Time");
|
||||
if (tag instanceof LongTag) {
|
||||
long days = ((LongTag) tag).getValue() / 24000;
|
||||
return i18n("world.info.time.format", days);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var timePane = new LineTextPane();
|
||||
{
|
||||
timePane.setTitle(i18n("world.info.time"));
|
||||
if (dataTag.get("Time") instanceof LongTag timeTag) {
|
||||
Duration duration = Duration.ofSeconds(timeTag.getValue() / 20);
|
||||
timePane.setText(i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart()));
|
||||
}
|
||||
}
|
||||
|
||||
var allowCheatsButton = new LineToggleButton();
|
||||
OptionToggleButton allowCheatsButton = new OptionToggleButton();
|
||||
{
|
||||
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
|
||||
allowCheatsButton.setDisable(isReadOnly);
|
||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = dataTag.get("allowCommands");
|
||||
|
||||
bindTagAndToggleButton(dataTag.get("allowCommands"), allowCheatsButton);
|
||||
checkTagAndSetListener(tag, allowCheatsButton);
|
||||
}
|
||||
|
||||
var generateFeaturesButton = new LineToggleButton();
|
||||
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
||||
{
|
||||
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
|
||||
generateFeaturesButton.setDisable(isReadOnly);
|
||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
||||
|
||||
// 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 {
|
||||
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
|
||||
}
|
||||
checkTagAndSetListener(tag, generateFeaturesButton);
|
||||
}
|
||||
|
||||
var difficultyButton = new LineSelectButton<Difficulty>();
|
||||
BorderPane difficultyPane = new BorderPane();
|
||||
{
|
||||
difficultyButton.setTitle(i18n("world.info.difficulty"));
|
||||
difficultyButton.setDisable(worldManagePage.isReadOnly());
|
||||
difficultyButton.setItems(Difficulty.items);
|
||||
setLeftLabel(difficultyPane, "world.info.difficulty");
|
||||
|
||||
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
|
||||
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
|
||||
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
||||
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
||||
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
|
||||
difficultyPane.setRight(difficultyBox);
|
||||
|
||||
Tag tag = dataTag.get("Difficulty");
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
||||
if (difficulty != null) {
|
||||
difficultyButton.setValue(difficulty);
|
||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
difficultyBox.setValue(difficulty);
|
||||
difficultyBox.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
difficultyTag.setValue((byte) newValue.ordinal());
|
||||
byteTag.setValue((byte) newValue.ordinal());
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
difficultyButton.setDisable(true);
|
||||
difficultyBox.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
difficultyButton.setDisable(true);
|
||||
difficultyBox.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
var difficultyLockPane = new LineToggleButton();
|
||||
OptionToggleButton difficultyLockPane = new OptionToggleButton();
|
||||
{
|
||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||
difficultyLockPane.setDisable(isReadOnly);
|
||||
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
|
||||
|
||||
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
|
||||
Tag tag = dataTag.get("DifficultyLocked");
|
||||
checkTagAndSetListener(tag, difficultyLockPane);
|
||||
}
|
||||
|
||||
worldInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane);
|
||||
basicInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
||||
}
|
||||
|
||||
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
|
||||
Tag playerTag = dataTag.get("Player");
|
||||
if (playerTag instanceof CompoundTag player) {
|
||||
ComponentList playerInfo = new ComponentList();
|
||||
|
||||
var locationPane = new LineTextPane();
|
||||
BorderPane locationPane = new BorderPane();
|
||||
{
|
||||
locationPane.setTitle(i18n("world.info.player.location"));
|
||||
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
|
||||
locationPane.setText(dimension.formatPosition(posTag));
|
||||
}
|
||||
}
|
||||
|
||||
var lastDeathLocationPane = new LineTextPane();
|
||||
{
|
||||
lastDeathLocationPane.setTitle(i18n("world.info.player.last_death_location"));
|
||||
// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||
if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) {
|
||||
Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension"));
|
||||
if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) {
|
||||
lastDeathLocationPane.setText(dimension.formatPosition(posTag));
|
||||
setLeftLabel(locationPane, "world.info.player.location");
|
||||
Label locationLabel = new Label();
|
||||
setRightTextLabel(locationPane, locationLabel, () -> {
|
||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(player.get("Pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
var spawnPane = new LineTextPane();
|
||||
BorderPane lastDeathLocationPane = new BorderPane();
|
||||
{
|
||||
spawnPane.setTitle(i18n("world.info.player.spawn"));
|
||||
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
|
||||
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
|
||||
&& intArrayTag.length() >= 3) { // Valid after 25w07a
|
||||
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
|
||||
} else if (playerTag.get("SpawnX") instanceof IntTag intX
|
||||
&& playerTag.get("SpawnY") instanceof IntTag intY
|
||||
&& playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
|
||||
spawnPane.setText((playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD)
|
||||
.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()));
|
||||
}
|
||||
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
||||
Label lastDeathLocationLabel = new Label();
|
||||
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
|
||||
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||
if (tag instanceof CompoundTag compoundTag) {
|
||||
Dimension dim = Dimension.of(compoundTag.get("dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(compoundTag.get("pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var playerGameTypePane = new LineSelectButton<GameType>();
|
||||
BorderPane spawnPane = new BorderPane();
|
||||
{
|
||||
playerGameTypePane.setTitle(i18n("world.info.player.game_type"));
|
||||
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
|
||||
playerGameTypePane.setItems(GameType.items);
|
||||
setLeftLabel(spawnPane, "world.info.player.spawn");
|
||||
Label spawnLabel = new Label();
|
||||
setRightTextLabel(spawnPane, spawnLabel, () -> {
|
||||
|
||||
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);
|
||||
Dimension dimension;
|
||||
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
|
||||
dimension = Dimension.of(respawnTag.get("dimension"));
|
||||
Tag posTag = respawnTag.get("pos");
|
||||
|
||||
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
|
||||
return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2));
|
||||
}
|
||||
} else if (player.get("SpawnX") instanceof IntTag intX
|
||||
&& player.get("SpawnY") instanceof IntTag intY
|
||||
&& player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
|
||||
dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension"));
|
||||
if (dimension == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
BorderPane playerGameTypePane = new BorderPane();
|
||||
{
|
||||
setLeftLabel(playerGameTypePane, "world.info.player.game_type");
|
||||
|
||||
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
||||
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
||||
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
|
||||
playerGameTypePane.setRight(gameTypeBox);
|
||||
|
||||
Tag tag = player.get("playerGameType");
|
||||
Tag hardcoreTag = dataTag.get("hardcore");
|
||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
||||
|
||||
if (tag instanceof IntTag intTag) {
|
||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
||||
if (gameType != null) {
|
||||
playerGameTypePane.setValue(gameType);
|
||||
playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
gameTypeBox.setValue(gameType);
|
||||
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
if (newValue == GameType.HARDCORE) {
|
||||
playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||
hardcoreTag.setValue((byte) 1);
|
||||
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 1);
|
||||
}
|
||||
} else {
|
||||
playerGameTypeTag.setValue(newValue.ordinal());
|
||||
hardcoreTag.setValue((byte) 0);
|
||||
intTag.setValue(newValue.ordinal());
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 0);
|
||||
}
|
||||
}
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
playerGameTypePane.setDisable(true);
|
||||
gameTypeBox.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
playerGameTypePane.setDisable(true);
|
||||
gameTypeBox.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
var healthPane = new LinePane();
|
||||
BorderPane healthPane = new BorderPane();
|
||||
{
|
||||
healthPane.setTitle(i18n("world.info.player.health"));
|
||||
setRightTextField(healthPane, 50, playerTag.get("Health"));
|
||||
setLeftLabel(healthPane, "world.info.player.health");
|
||||
JFXTextField healthField = new JFXTextField();
|
||||
setRightTextField(healthPane, healthField, 50);
|
||||
|
||||
Tag tag = player.get("Health");
|
||||
if (tag instanceof FloatTag floatTag) {
|
||||
setTagAndTextField(floatTag, healthField);
|
||||
} else {
|
||||
healthField.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
var foodLevelPane = new LinePane();
|
||||
BorderPane foodLevelPane = new BorderPane();
|
||||
{
|
||||
foodLevelPane.setTitle(i18n("world.info.player.food_level"));
|
||||
setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel"));
|
||||
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
||||
JFXTextField foodLevelField = new JFXTextField();
|
||||
setRightTextField(foodLevelPane, foodLevelField, 50);
|
||||
|
||||
Tag tag = player.get("foodLevel");
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, foodLevelField);
|
||||
} else {
|
||||
foodLevelField.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
var foodSaturationPane = new LinePane();
|
||||
BorderPane xpLevelPane = new BorderPane();
|
||||
{
|
||||
foodSaturationPane.setTitle(i18n("world.info.player.food_saturation_level"));
|
||||
setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel"));
|
||||
}
|
||||
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
||||
JFXTextField xpLevelField = new JFXTextField();
|
||||
setRightTextField(xpLevelPane, xpLevelField, 50);
|
||||
|
||||
var xpLevelPane = new LinePane();
|
||||
{
|
||||
xpLevelPane.setTitle(i18n("world.info.player.xp_level"));
|
||||
setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel"));
|
||||
Tag tag = player.get("XpLevel");
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, xpLevelField);
|
||||
} else {
|
||||
xpLevelField.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
playerInfo.getContent().setAll(
|
||||
locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane,
|
||||
healthPane, foodLevelPane, foodSaturationPane, xpLevelPane
|
||||
locationPane, lastDeathLocationPane, spawnPane,
|
||||
playerGameTypePane, healthPane, foodLevelPane, xpLevelPane
|
||||
);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) {
|
||||
JFXTextField textField = new JFXTextField();
|
||||
setRightTextField(borderPane, textField, perfWidth);
|
||||
if (tag instanceof IntTag intTag) {
|
||||
bindTagAndTextField(intTag, textField);
|
||||
} else if (tag instanceof FloatTag floatTag) {
|
||||
bindTagAndTextField(floatTag, textField);
|
||||
} else {
|
||||
textField.setDisable(true);
|
||||
}
|
||||
private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = "assets.lang.I18N") String key) {
|
||||
Label label = new Label(i18n(key));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(label);
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
||||
textField.setDisable(isReadOnly);
|
||||
textField.setDisable(worldManagePage.isReadOnly());
|
||||
textField.setPrefWidth(perfWidth);
|
||||
BorderPane.setAlignment(textField, Pos.CENTER_RIGHT);
|
||||
textField.setAlignment(Pos.CENTER_RIGHT);
|
||||
borderPane.setRight(textField);
|
||||
}
|
||||
|
||||
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
|
||||
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
|
||||
FXUtils.copyOnDoubleClick(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
||||
try {
|
||||
label.setText(setNameCall.call());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Exception happened when setting name", e);
|
||||
}
|
||||
borderPane.setRight(label);
|
||||
}
|
||||
|
||||
private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) {
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
@@ -451,17 +511,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
}
|
||||
|
||||
private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(intTag.getValue().toString());
|
||||
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
Integer integer = Lang.toIntOrNull(newValue);
|
||||
if (integer != null) {
|
||||
intTag.setValue(integer);
|
||||
saveLevelDat();
|
||||
}
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
@@ -472,17 +529,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue()));
|
||||
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
Float floatValue = Lang.toFloatOrNull(newValue);
|
||||
if (floatValue != null) {
|
||||
floatTag.setValue(floatValue);
|
||||
saveLevelDat();
|
||||
}
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
@@ -502,23 +556,6 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
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) {
|
||||
static final Dimension OVERWORLD = new Dimension(null);
|
||||
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
|
||||
@@ -528,8 +565,8 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
if (tag instanceof IntTag intTag) {
|
||||
return switch (intTag.getValue()) {
|
||||
case 0 -> OVERWORLD;
|
||||
case -1 -> THE_NETHER;
|
||||
case 1 -> THE_END;
|
||||
case 1 -> THE_NETHER;
|
||||
case 2 -> THE_END;
|
||||
default -> null;
|
||||
};
|
||||
} else if (tag instanceof StringTag stringTag) {
|
||||
@@ -625,12 +662,12 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||
fileChooser.setInitialFileName("icon.png");
|
||||
|
||||
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
|
||||
if (iconPath == null) return;
|
||||
File file = fileChooser.showOpenDialog(Controllers.getStage());
|
||||
if (file == null) return;
|
||||
|
||||
Image image;
|
||||
try {
|
||||
image = FXUtils.loadImage(iconPath);
|
||||
image = FXUtils.loadImage(file.toPath());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load image", e);
|
||||
Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR);
|
||||
@@ -638,16 +675,16 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
saveWorldIcon(iconPath, image, output);
|
||||
saveImage(image, output);
|
||||
} else {
|
||||
Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) {
|
||||
private void saveImage(Image image, Path path) {
|
||||
Image oldImage = iconImageView.getImage();
|
||||
try {
|
||||
FileUtils.copyFile(sourcePath, targetPath);
|
||||
PNGJavaFXUtils.writeImage(image, path);
|
||||
iconImageView.setImage(image);
|
||||
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
@@ -64,9 +65,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
|
||||
private Path savesDir;
|
||||
private Path backupsDir;
|
||||
private List<World> worlds;
|
||||
private Profile profile;
|
||||
private String instanceId;
|
||||
private String id;
|
||||
|
||||
private int refreshCount = 0;
|
||||
|
||||
@@ -86,8 +88,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
@Override
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.instanceId = id;
|
||||
this.id = id;
|
||||
this.savesDir = profile.getRepository().getSavesDirectory(id);
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(id);
|
||||
refresh();
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
} else if (showAll.get()) {
|
||||
getItems().setAll(worlds);
|
||||
} else {
|
||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
getItems().setAll(worlds.stream()
|
||||
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.toList());
|
||||
@@ -105,7 +108,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (profile == null || instanceId == null)
|
||||
if (profile == null || id == null)
|
||||
return;
|
||||
|
||||
int currentRefresh = ++refreshCount;
|
||||
@@ -113,8 +116,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
setLoading(true);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
// Ensure the game version number is parsed
|
||||
profile.getRepository().getGameVersion(instanceId);
|
||||
return World.getWorlds(savesDir);
|
||||
profile.getRepository().getGameVersion(id);
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.toList();
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (refreshCount != currentRefresh) {
|
||||
// A newer refresh task is running, discard this result
|
||||
@@ -151,18 +156,18 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
// Or too many input dialogs are popped.
|
||||
Task.supplyAsync(() -> new World(zipFile))
|
||||
.whenComplete(Schedulers.javafx(), world -> {
|
||||
Controllers.prompt(i18n("world.name.enter"), (name, handler) -> {
|
||||
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
|
||||
Task.runAsync(() -> world.install(savesDir, name))
|
||||
.whenComplete(Schedulers.javafx(), () -> {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
refresh();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
||||
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
else
|
||||
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, e -> {
|
||||
@@ -172,7 +177,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
private void showManagePage(World world) {
|
||||
Controllers.navigate(new WorldManagePage(world, profile, instanceId));
|
||||
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
|
||||
}
|
||||
|
||||
public void export(World world) {
|
||||
@@ -192,11 +197,11 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
public void launch(World world) {
|
||||
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript(World world) {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
public BooleanProperty showAllProperty() {
|
||||
@@ -211,15 +216,14 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(WorldListPage skinnable) {
|
||||
JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all"));
|
||||
JFXCheckBox chkShowAll = new JFXCheckBox();
|
||||
chkShowAll.setText(i18n("world.show_all"));
|
||||
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
|
||||
|
||||
return Arrays.asList(
|
||||
chkShowAll,
|
||||
return Arrays.asList(chkShowAll,
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)
|
||||
);
|
||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,7 +240,6 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
private final ImageView imageView;
|
||||
private final Tooltip leftTooltip;
|
||||
private final TwoLineListItem content;
|
||||
private final JFXButton btnLaunch;
|
||||
|
||||
public WorldListCell(WorldListPage page) {
|
||||
this.page = page;
|
||||
@@ -268,17 +271,6 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
root.setRight(right);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnLaunch = new JFXButton();
|
||||
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();
|
||||
if (world != null)
|
||||
page.launch(world);
|
||||
});
|
||||
|
||||
JFXButton btnMore = new JFXButton();
|
||||
right.getChildren().add(btnMore);
|
||||
btnMore.getStyleClass().add("toggle-icon4");
|
||||
@@ -325,12 +317,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
if (world.isLocked()) {
|
||||
if (world.isLocked())
|
||||
content.addTag(i18n("world.locked"));
|
||||
btnLaunch.setDisable(true);
|
||||
} else {
|
||||
btnLaunch.setDisable(false);
|
||||
}
|
||||
|
||||
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||
|
||||
@@ -341,15 +329,13 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
// Popup Menu
|
||||
|
||||
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
boolean worldLocked = world.isLocked();
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
|
||||
if (world.supportQuickPlay()) {
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
launchItem.setDisable(worldLocked);
|
||||
launchItem.setDisable(world.isLocked());
|
||||
popupMenu.getContent().add(launchItem);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
@@ -368,19 +354,18 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (ChunkBaseApp.supportEndCity(world)) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
}
|
||||
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
||||
exportMenuItem.setDisable(worldLocked);
|
||||
|
||||
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);
|
||||
duplicateMenuItem.setDisable(worldLocked);
|
||||
boolean worldLocked = world.isLocked();
|
||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -35,7 +37,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
@@ -49,92 +50,143 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
private final ObjectProperty<State> state;
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final Profile profile;
|
||||
private final String versionId;
|
||||
private FileChannel sessionLockChannel;
|
||||
private final String id;
|
||||
|
||||
private final ObjectProperty<State> state;
|
||||
private boolean isFirstNavigation = true;
|
||||
private final BooleanProperty refreshable = new SimpleBooleanProperty(true);
|
||||
private final BooleanProperty readOnly = new SimpleBooleanProperty(false);
|
||||
private boolean loadFailed = false;
|
||||
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final TabHeader header = new TabHeader(transitionPane);
|
||||
private final TabHeader header;
|
||||
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
||||
|
||||
public WorldManagePage(World world, Profile profile, String versionId) {
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
|
||||
this.world = world;
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(versionId);
|
||||
this.backupsDir = backupsDir;
|
||||
this.profile = profile;
|
||||
this.versionId = versionId;
|
||||
|
||||
updateSessionLockChannel();
|
||||
this.id = id;
|
||||
|
||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||
try {
|
||||
this.world.reloadLevelDat();
|
||||
world.reloadLevelDat();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
|
||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||
loadFailed = true;
|
||||
}
|
||||
|
||||
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
|
||||
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))));
|
||||
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
|
||||
header.select(worldInfoTab);
|
||||
|
||||
setCenter(transitionPane);
|
||||
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
VBox.setVgrow(left, Priority.ALWAYS);
|
||||
setLeft(left);
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
||||
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
|
||||
header.getTabs().add(datapackTab);
|
||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
|
||||
left.setTop(sideBar);
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(world)) {
|
||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||
|
||||
chunkBasePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), chunkBasePopup)
|
||||
);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
chunkBasePopupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||
chunkBaseMenuItem.setOnAction(e ->
|
||||
chunkBasePopup.show(chunkBaseMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
chunkBaseMenuItem.getWidth(), 0)));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null);
|
||||
|
||||
{
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup)
|
||||
);
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
|
||||
{
|
||||
managePopupMenuItem.setOnAction(e ->
|
||||
managePopup.show(managePopupMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
managePopupMenuItem.getWidth(), 0));
|
||||
managePopupMenuItem.setDisable(isReadOnly());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
left.setBottom(toolbar);
|
||||
|
||||
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull Skin createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
updateSessionLockChannel();
|
||||
try {
|
||||
world.reloadLevelDat();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||
closePageForLoadingFail();
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (loadFailed) {
|
||||
Platform.runLater(() -> {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (var tab : header.getTabs()) {
|
||||
if (tab.getNode() instanceof WorldRefreshable r) {
|
||||
r.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closePageForLoadingFail() {
|
||||
Platform.runLater(() -> {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSessionLockChannel() {
|
||||
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
|
||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||
readOnly.set(sessionLockChannel == null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (isFirstNavigation)
|
||||
isFirstNavigation = false;
|
||||
else
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void onExited(Navigator.NavigationEvent event) {
|
||||
try {
|
||||
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
|
||||
@@ -142,24 +194,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
}
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, versionId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.state.set(new DecoratorPage.State(title, null, true, true, true));
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
@@ -169,123 +208,15 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return readOnly.get();
|
||||
return sessionLockChannel == null;
|
||||
}
|
||||
|
||||
public BooleanProperty readOnlyProperty() {
|
||||
return readOnly;
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BooleanProperty refreshableProperty() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public static class Skin extends DecoratorAnimatedPageSkin<WorldManagePage> {
|
||||
|
||||
protected Skin(WorldManagePage control) {
|
||||
super(control);
|
||||
|
||||
setCenter(control.transitionPane);
|
||||
setLeft(getSidebar());
|
||||
}
|
||||
|
||||
private BorderPane getSidebar() {
|
||||
BorderPane sidebar = new BorderPane();
|
||||
{
|
||||
FXUtils.setLimitWidth(sidebar, 200);
|
||||
VBox.setVgrow(sidebar, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
sidebar.setTop(getTabBar());
|
||||
sidebar.setBottom(getToolBar());
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
private AdvancedListBox getTabBar() {
|
||||
AdvancedListBox tabBar = new AdvancedListBox();
|
||||
{
|
||||
getSkinnable().header.getTabs().addAll(getSkinnable().worldInfoTab, getSkinnable().worldBackupsTab);
|
||||
getSkinnable().header.select(getSkinnable().worldInfoTab);
|
||||
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (getSkinnable().world.supportDatapacks()) {
|
||||
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
}
|
||||
|
||||
return tabBar;
|
||||
}
|
||||
|
||||
private AdvancedListBox getToolBar() {
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
{
|
||||
if (getSkinnable().world.supportQuickPlay()) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(getSkinnable().world)) {
|
||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||
|
||||
chunkBasePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(getSkinnable().world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(getSkinnable().world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(getSkinnable().world), chunkBasePopup)
|
||||
);
|
||||
|
||||
if (ChunkBaseApp.supportEndCity(getSkinnable().world)) {
|
||||
chunkBasePopupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(getSkinnable().world), chunkBasePopup));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||
chunkBaseMenuItem.setOnAction(e ->
|
||||
chunkBasePopup.show(chunkBaseMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
chunkBaseMenuItem.getWidth(), 0)));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(getSkinnable().world.getFile()));
|
||||
|
||||
{
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (getSkinnable().world.supportQuickPlay()) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, 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)
|
||||
);
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
|
||||
{
|
||||
managePopupMenuItem.setOnAction(e ->
|
||||
managePopup.show(managePopupMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
managePopupMenuItem.getWidth(), 0));
|
||||
managePopupMenuItem.disableProperty().bind(getSkinnable().readOnlyProperty());
|
||||
});
|
||||
}
|
||||
}
|
||||
return toolbar;
|
||||
}
|
||||
}
|
||||
|
||||
public interface WorldRefreshable {
|
||||
void refresh();
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
}
|
||||
@@ -92,20 +92,20 @@ public final class WorldManageUIUtils {
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
i18n("world.duplicate.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
(result, resolve, reject) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.empty_name"));
|
||||
reject.accept(i18n("world.duplicate.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.invalid_name"));
|
||||
reject.accept(i18n("world.duplicate.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = worldPath.resolveSibling(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("world.duplicate.failed.already_exists"));
|
||||
reject.accept(i18n("world.duplicate.failed.already_exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ public final class WorldManageUIUtils {
|
||||
}
|
||||
).whenComplete(Schedulers.javafx(), (throwable) -> {
|
||||
if (throwable == null) {
|
||||
handler.resolve();
|
||||
resolve.run();
|
||||
} else {
|
||||
handler.reject(i18n("world.duplicate.failed"));
|
||||
reject.accept(i18n("world.duplicate.failed"));
|
||||
LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
|
||||
}
|
||||
})
|
||||
@@ -144,7 +144,7 @@ public final class WorldManageUIUtils {
|
||||
FileChannel lock = world.lock();
|
||||
LOG.info("Acquired lock on world " + world.getFileName());
|
||||
return lock;
|
||||
} catch (WorldLockedException ignored) {
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ public final class ChunkBaseApp {
|
||||
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
|
||||
|
||||
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
||||
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
|
||||
|
||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
||||
@@ -53,11 +52,6 @@ public final class ChunkBaseApp {
|
||||
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
|
||||
}
|
||||
|
||||
public static boolean supportEndCity(@NotNull World world) {
|
||||
return world.getSeed() != null && world.getGameVersion() != null &&
|
||||
world.getGameVersion().compareTo(MIN_END_CITY_VERSION) >= 0;
|
||||
}
|
||||
|
||||
public static ChunkBaseApp newBuilder(String app, long seed) {
|
||||
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ public final class NativePatcher {
|
||||
public static SupportStatus checkSupportedStatus(GameVersionNumber gameVersion, Platform platform,
|
||||
OSVersion systemVersion) {
|
||||
if (platform.equals(Platform.WINDOWS_X86_64)) {
|
||||
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_7) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_10) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
@@ -299,11 +299,6 @@
|
||||
-fx-fill: -monet-surface-tint;
|
||||
}
|
||||
|
||||
.popup-menu .scroll-pane > .scroll-bar:vertical,
|
||||
.popup-menu .scroll-pane > .scroll-bar:horizontal {
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
.popup-menu-content {
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
@@ -955,13 +950,6 @@
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.no-horizontal-scrollbar .scroll-bar:horizontal {
|
||||
-fx-opacity: 0;
|
||||
-fx-max-height: 0;
|
||||
-fx-min-height: 0;
|
||||
-fx-pref-height: 0;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
-fx-background-color: transparent;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
||||
@@ -1011,6 +999,10 @@
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
||||
}
|
||||
|
||||
.card:selected {
|
||||
-fx-background-color: -monet-secondary-container-transparent-80;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
-fx-padding: 10;
|
||||
-fx-spacing: 10;
|
||||
@@ -1857,35 +1849,3 @@
|
||||
.tooltip .text {
|
||||
-fx-fill: -monet-inverse-on-surface;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Line Button *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
|
||||
.line-select-button .svg {
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.line-select-button .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .title {
|
||||
-fx-font-size: 13px;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .title {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .subtitle {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .subtitle {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user