Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a795b2c8 | ||
|
|
46f7e4db66 | ||
|
|
d956c04f2a | ||
|
|
59e147227d | ||
|
|
4cbe874c81 | ||
|
|
e9e6ef1f20 | ||
|
|
d2ae41c9ae | ||
|
|
79a1c3af8a | ||
|
|
c06f898d39 | ||
|
|
b09b915d16 | ||
|
|
179ccb7805 | ||
|
|
f7987a91ce | ||
|
|
e68b96f67d | ||
|
|
7f6379960b | ||
|
|
317b92c31e | ||
|
|
35e65921f9 | ||
|
|
95eeae4c42 | ||
|
|
165d77df30 | ||
|
|
65a4b6159d | ||
|
|
6f690d2522 | ||
|
|
5a34ac0d2b | ||
|
|
0ccb352b7e |
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) 获取帮助。
|
||||
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
|
||||
* 该问题确实是 **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) 下载最新预览版本。
|
||||
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 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 click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
|
||||
* 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).
|
||||
|
||||
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: |
|
||||
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||
如果你的启动器崩溃了,请将崩溃报告填入 (或将文件拖入) 下方。
|
||||
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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,8 +24,10 @@ 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;
|
||||
@@ -41,7 +43,12 @@ import javafx.stage.Window;
|
||||
public class JFXPopup extends PopupControl {
|
||||
|
||||
public enum PopupHPosition {
|
||||
RIGHT, LEFT
|
||||
RIGHT,
|
||||
LEFT;
|
||||
|
||||
public PopupHPosition getOpposite() {
|
||||
return (this == RIGHT) ? LEFT : RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PopupVPosition {
|
||||
@@ -121,18 +128,21 @@ 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()) {
|
||||
if (node.getScene() == null || node.getScene().getWindow() == null) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null || scene.getWindow() == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = node.getScene().getWindow();
|
||||
Window parent = scene.getWindow();
|
||||
final Point2D origin = node.localToScene(0, 0);
|
||||
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);
|
||||
|
||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
||||
|
||||
this.show(parent,
|
||||
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);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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
@@ -51,8 +51,11 @@ public final class EntryPoint {
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
|
||||
initIcon();
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
||||
if (!isInsideMacAppBundle())
|
||||
initIcon();
|
||||
}
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
|
||||
@@ -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, resolve, reject) -> {
|
||||
(result, handler) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
try (Zipper zipper = new Zipper(zipFile, true)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -24,7 +25,10 @@ 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;
|
||||
@@ -32,11 +36,17 @@ 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.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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@@ -65,7 +75,7 @@ public final class Themes {
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
yield FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT;
|
||||
} else {
|
||||
yield Brightness.DEFAULT;
|
||||
yield getDefaultBrightness();
|
||||
}
|
||||
}
|
||||
case "dark" -> Brightness.DARK;
|
||||
@@ -97,6 +107,34 @@ public final class Themes {
|
||||
theme.addListener(listener);
|
||||
}
|
||||
|
||||
private static Brightness defaultBrightness;
|
||||
|
||||
private static Brightness getDefaultBrightness() {
|
||||
if (defaultBrightness != null)
|
||||
return defaultBrightness;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultBrightness = brightness;
|
||||
}
|
||||
|
||||
public static ObjectExpression<Theme> themeProperty() {
|
||||
return theme;
|
||||
}
|
||||
@@ -129,6 +167,39 @@ 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,7 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -26,7 +27,10 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
@@ -39,20 +43,23 @@ 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.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -61,9 +68,13 @@ 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.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
@@ -88,12 +99,14 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
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.*;
|
||||
@@ -102,8 +115,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.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class FXUtils {
|
||||
private FXUtils() {
|
||||
@@ -420,6 +433,20 @@ 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);
|
||||
@@ -1609,4 +1636,39 @@ 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,6 +40,7 @@ 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;
|
||||
@@ -90,6 +91,8 @@ 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;
|
||||
|
||||
@@ -21,7 +21,6 @@ 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;
|
||||
@@ -29,7 +28,6 @@ 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;
|
||||
@@ -38,7 +36,6 @@ 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;
|
||||
@@ -48,9 +45,6 @@ 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;
|
||||
@@ -87,41 +81,13 @@ 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,7 +50,9 @@ 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"),
|
||||
@@ -102,6 +104,7 @@ 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"),
|
||||
@@ -121,6 +124,7 @@ public enum SVG {
|
||||
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"),
|
||||
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"),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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,6 +118,7 @@ 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(
|
||||
|
||||
@@ -82,7 +82,9 @@ public class TransitionPane extends StackPane {
|
||||
duration, interpolator);
|
||||
newAnimation.setOnFinished(e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
if (previousNode != currentNode) {
|
||||
getChildren().remove(previousNode);
|
||||
}
|
||||
|
||||
if (cacheHint != null) {
|
||||
newView.setCache(false);
|
||||
|
||||
@@ -19,8 +19,6 @@ 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;
|
||||
@@ -28,7 +26,6 @@ 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;
|
||||
@@ -42,8 +39,7 @@ import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
final class ComponentListCell extends StackPane {
|
||||
private final Node content;
|
||||
private Animation expandAnimation;
|
||||
private Rectangle clipRect;
|
||||
private final BooleanProperty expanded = new SimpleBooleanProperty(this, "expanded", false);
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentListCell(Node content) {
|
||||
this.content = content;
|
||||
@@ -51,25 +47,6 @@ final class ComponentListCell extends StackPane {
|
||||
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");
|
||||
@@ -147,8 +124,8 @@ final class ComponentListCell extends StackPane {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
boolean expanded = !isExpanded();
|
||||
setExpanded(expanded);
|
||||
boolean expanded = !this.expanded;
|
||||
this.expanded = expanded;
|
||||
if (expanded) {
|
||||
list.doLazyInit();
|
||||
list.layout();
|
||||
@@ -157,14 +134,9 @@ final class ComponentListCell extends StackPane {
|
||||
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);
|
||||
@@ -177,19 +149,11 @@ final class ComponentListCell extends StackPane {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -200,16 +164,4 @@ final class ComponentListCell extends StackPane {
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return expanded.get();
|
||||
}
|
||||
|
||||
public BooleanProperty expandedProperty() {
|
||||
return expanded;
|
||||
}
|
||||
|
||||
public void setExpanded(boolean expanded) {
|
||||
this.expanded.set(expanded);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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,13 +72,19 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
||||
acceptButton.setOnAction(e -> {
|
||||
acceptPane.showSpinner();
|
||||
|
||||
onResult.call(textField.getText(), () -> {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(textField.getText());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, msg -> {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(msg);
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
textField.setOnAction(event -> acceptButton.fire());
|
||||
|
||||
@@ -78,6 +78,8 @@ 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,11 +117,17 @@ public class PromptDialogPane extends DialogPane {
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
|
||||
builder.callback.call(builder.questions, () -> {
|
||||
future.complete(builder.questions);
|
||||
runInFX(this::onSuccess);
|
||||
}, msg -> {
|
||||
runInFX(() -> onFailure(msg));
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ 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) {
|
||||
@@ -59,7 +62,7 @@ public class TwoLineListItem extends VBox {
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
|
||||
Label lblTitle = new Label();
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
@@ -68,7 +71,7 @@ public class TwoLineListItem extends VBox {
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
|
||||
@@ -109,6 +112,14 @@ 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");
|
||||
|
||||
@@ -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, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||
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);
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
|
||||
}
|
||||
|
||||
@@ -124,37 +124,37 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
private void onChooseRemoteFile() {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, handler) -> {
|
||||
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) {
|
||||
reject.accept(i18n("modpack.type.server.malformed"));
|
||||
handler.reject(i18n("modpack.type.server.malformed"));
|
||||
} else if (e == null) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest);
|
||||
controller.onNext();
|
||||
} else {
|
||||
reject.accept(e.getMessage());
|
||||
handler.reject(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");
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
controller.getSettings().put(MODPACK_FILE, modpack);
|
||||
controller.onNext();
|
||||
} else {
|
||||
reject.accept(e.getMessage());
|
||||
handler.reject(e.getMessage());
|
||||
}
|
||||
}).executor(true),
|
||||
i18n("message.downloading"),
|
||||
@@ -162,7 +162,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
reject.accept(e.getMessage());
|
||||
handler.reject(e.getMessage());
|
||||
}
|
||||
}, "", new URLValidator());
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
JavaPageSkin(JavaManagementPage skinnable) {
|
||||
super(skinnable);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,7 +211,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
@Override
|
||||
protected ListCell<JavaRuntime> createListCell(JFXListView<JavaRuntime> listView) {
|
||||
return new JavaItemCell();
|
||||
return new JavaItemCell(listView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
private final StackPane removeIconPane;
|
||||
private final Tooltip removeTooltip = new Tooltip();
|
||||
|
||||
JavaItemCell() {
|
||||
JavaItemCell(JFXListView<JavaRuntime> listView) {
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
HBox center = new HBox();
|
||||
@@ -272,6 +273,8 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
this.graphic = new RipplerContainer(root);
|
||||
|
||||
FXUtils.limitCellWidth(listView, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -153,11 +153,13 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ 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;
|
||||
|
||||
@@ -87,11 +86,8 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
brightnessPane.setLeft(left);
|
||||
|
||||
JFXComboBox<String> cboBrightness = new JFXComboBox<>(FXCollections.observableArrayList(
|
||||
FXUtils.DARK_MODE != null
|
||||
? List.of("auto", "light", "dark")
|
||||
: List.of("light", "dark")
|
||||
));
|
||||
JFXComboBox<String> cboBrightness = new JFXComboBox<>();
|
||||
cboBrightness.getItems().setAll("auto", "light", "dark");
|
||||
cboBrightness.setConverter(FXUtils.stringConverter(name -> i18n("settings.launcher.brightness." + name)));
|
||||
cboBrightness.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
brightnessPane.setRight(cboBrightness);
|
||||
|
||||
@@ -19,9 +19,7 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
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;
|
||||
@@ -37,11 +35,11 @@ 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;
|
||||
@@ -148,7 +146,7 @@ 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, () -> showAccountListPopupMenu(accountListItem));
|
||||
FXUtils.onSecondaryButtonClicked(accountListItem, () -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, accountListItem.getWidth(), 0));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
// second item in left sidebar
|
||||
@@ -251,36 +249,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
setCenter(getSkinnable().getMainPage());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
|
||||
GameListPopupMenu.show(gameListItem,
|
||||
JFXPopup.PopupVPosition.TOP,
|
||||
|
||||
@@ -63,9 +63,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
@@ -330,6 +328,19 @@ 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`;
|
||||
@@ -389,6 +400,8 @@ 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, '.');
|
||||
@@ -410,7 +423,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
input = null;
|
||||
}
|
||||
|
||||
String entryName = StringUtils.substringBeforeLast(fileName, ".");
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
@@ -427,10 +440,10 @@ public final class SettingsPage extends ScrollPane {
|
||||
continue;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, fileName, input, buffer);
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry("hmcl-latest.log"));
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
@@ -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, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
|
||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
||||
if (e != null) {
|
||||
reject.accept(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||
handler.reject(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||
} else {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
} else {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,12 +25,15 @@ 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;
|
||||
@@ -69,7 +72,13 @@ 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 -> {
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.*;
|
||||
@@ -30,11 +28,9 @@ 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.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -52,7 +48,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.FloatListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -525,46 +521,65 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
||||
FXUtils.onClicked(listView, () -> {
|
||||
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
RemoteMod selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
Controllers.navigate(new DownloadPage(getSkinnable(), selectedItem, getSkinnable().getProfileVersion(), getSkinnable().callback));
|
||||
});
|
||||
|
||||
listView.setSelectionModel(null);
|
||||
// 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 FloatListCell<>(listView) {
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
private static final Insets PADDING = new Insets(9, 9, 0, 9);
|
||||
|
||||
private final RipplerContainer graphic;
|
||||
|
||||
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);
|
||||
pane.getChildren().add(container);
|
||||
JFXDepthManager.setDepth(container, 1);
|
||||
|
||||
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 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));
|
||||
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);
|
||||
}
|
||||
iconLoader.load(imageView.imageProperty(), dataItem.getIconUrl());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ 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);
|
||||
|
||||
@@ -81,6 +81,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
{
|
||||
HBox center = new HBox();
|
||||
center.setMouseTransparent(true);
|
||||
root.setCenter(center);
|
||||
center.setPrefWidth(Region.USE_PREF_SIZE);
|
||||
center.setSpacing(8);
|
||||
|
||||
@@ -163,30 +163,30 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
i18n("schematics.create_directory.prompt"),
|
||||
"",
|
||||
(result, resolve, reject) -> {
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
reject.accept(i18n("schematics.create_directory.failed.empty_name"));
|
||||
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
reject.accept(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = parent.resolve(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
reject.accept(i18n("schematics.create_directory.failed.already_exists"));
|
||||
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(targetDir);
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
refresh();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||
reject.accept(i18n("schematics.create_directory.failed", targetDir));
|
||||
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ 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.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.ServerAddress;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
@@ -56,34 +53,18 @@ 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;
|
||||
@@ -127,6 +108,8 @@ 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);
|
||||
@@ -510,10 +493,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
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
|
||||
@@ -809,4 +789,58 @@ 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, resolve, reject) -> {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
|
||||
if (newName.equals(version)) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
return;
|
||||
}
|
||||
if (profile.getRepository().renameVersion(version, newName)) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
profile.getRepository().refreshVersionsAsync()
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
if (profile.getRepository().hasVersion(newName)) {
|
||||
@@ -143,7 +143,7 @@ public final class Versions {
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
reject.accept(i18n("version.manage.rename.fail"));
|
||||
handler.reject(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, resolve, reject) -> {
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||
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) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
} else {
|
||||
reject.accept(StringUtils.getStackTrace(exception));
|
||||
handler.reject(StringUtils.getStackTrace(exception));
|
||||
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
|
||||
profile.getRepository().removeVersionFromDisk(newVersionName);
|
||||
}
|
||||
|
||||
@@ -156,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, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("world.name.enter"), (name, handler) -> {
|
||||
Task.runAsync(() -> world.install(savesDir, name))
|
||||
.whenComplete(Schedulers.javafx(), () -> {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
refresh();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
||||
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
else
|
||||
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, e -> {
|
||||
|
||||
@@ -92,20 +92,20 @@ public final class WorldManageUIUtils {
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
i18n("world.duplicate.prompt"),
|
||||
"",
|
||||
(result, resolve, reject) -> {
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
reject.accept(i18n("world.duplicate.failed.empty_name"));
|
||||
handler.reject(i18n("world.duplicate.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
reject.accept(i18n("world.duplicate.failed.invalid_name"));
|
||||
handler.reject(i18n("world.duplicate.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = worldPath.resolveSibling(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
reject.accept(i18n("world.duplicate.failed.already_exists"));
|
||||
handler.reject(i18n("world.duplicate.failed.already_exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ public final class WorldManageUIUtils {
|
||||
}
|
||||
).whenComplete(Schedulers.javafx(), (throwable) -> {
|
||||
if (throwable == null) {
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
} else {
|
||||
reject.accept(i18n("world.duplicate.failed"));
|
||||
handler.reject(i18n("world.duplicate.failed"));
|
||||
LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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_10) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_7) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
@@ -299,6 +299,11 @@
|
||||
-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;
|
||||
}
|
||||
@@ -950,6 +955,13 @@
|
||||
-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);
|
||||
@@ -999,10 +1011,6 @@
|
||||
-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;
|
||||
|
||||
@@ -1108,6 +1108,14 @@ mods.install=Install
|
||||
mods.save_as=Save As
|
||||
mods.unknown=Unknown Mod
|
||||
|
||||
menu.undo=Undo
|
||||
menu.redo=Redo
|
||||
menu.cut=Cut
|
||||
menu.copy=Copy
|
||||
menu.paste=Paste
|
||||
menu.deleteselection=Delete
|
||||
menu.selectall=Select All
|
||||
|
||||
nbt.entries=%s entries
|
||||
nbt.open.failed=Failed to open file
|
||||
nbt.save.failed=Failed to save file
|
||||
|
||||
@@ -896,6 +896,14 @@ mods.install=安裝到目前實例
|
||||
mods.save_as=下載到本機目錄
|
||||
mods.unknown=未知模組
|
||||
|
||||
menu.undo=還原
|
||||
menu.redo=重做
|
||||
menu.cut=剪下
|
||||
menu.copy=複製
|
||||
menu.paste=貼上
|
||||
menu.deleteselection=刪除
|
||||
menu.selectall=全選
|
||||
|
||||
nbt.entries=%s 個條目
|
||||
nbt.open.failed=開啟檔案失敗
|
||||
nbt.save.failed=儲存檔案失敗
|
||||
|
||||
@@ -900,6 +900,14 @@ mods.install=安装到当前实例
|
||||
mods.save_as=下载到本地文件夹
|
||||
mods.unknown=未知模组
|
||||
|
||||
menu.undo=撤销
|
||||
menu.redo=重做
|
||||
menu.cut=剪切
|
||||
menu.copy=复制
|
||||
menu.paste=粘贴
|
||||
menu.deleteselection=删除
|
||||
menu.selectall=全选
|
||||
|
||||
nbt.entries=%s 个条目
|
||||
nbt.open.failed=打开文件失败
|
||||
nbt.save.failed=保存文件失败
|
||||
|
||||
@@ -182,7 +182,7 @@ public final class ModManager {
|
||||
if (Files.isDirectory(getModsDirectory())) {
|
||||
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
|
||||
for (Path subitem : modsDirectoryStream) {
|
||||
if (supportSubfolders && Files.isDirectory(subitem)) {
|
||||
if (supportSubfolders && Files.isDirectory(subitem) && !".connector".equalsIgnoreCase(subitem.getFileName().toString())) {
|
||||
try (DirectoryStream<Path> subitemDirectoryStream = Files.newDirectoryStream(subitem)) {
|
||||
for (Path subsubitem : subitemDirectoryStream) {
|
||||
addModInfo(subsubitem);
|
||||
|
||||
@@ -17,18 +17,23 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FutureCallback<T> {
|
||||
|
||||
/**
|
||||
* Callback of future, called after future finishes.
|
||||
* This callback gives the feedback whether the result of future is acceptable or not,
|
||||
* if not, giving the reason, and future will be relaunched when necessary.
|
||||
* @param result result of the future
|
||||
* @param resolve accept the result
|
||||
* @param reject reject the result with failure reason
|
||||
*/
|
||||
void call(T result, Runnable resolve, Consumer<String> reject);
|
||||
/// Callback of future, called after future finishes.
|
||||
/// This callback gives the feedback whether the result of future is acceptable or not,
|
||||
/// if not, giving the reason, and future will be relaunched when necessary.
|
||||
///
|
||||
/// @param result result of the future
|
||||
/// @param handler handler to accept or reject the result
|
||||
void call(T result, ResultHandler handler);
|
||||
|
||||
interface ResultHandler {
|
||||
|
||||
/// Accept the result.
|
||||
void resolve();
|
||||
|
||||
/// Reject the result with given reason.
|
||||
void reject(String reason);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
@@ -39,13 +41,15 @@ public final class Zipper implements Closeable {
|
||||
|
||||
private final ZipOutputStream zos;
|
||||
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
private final Set<String> entryNames;
|
||||
|
||||
public Zipper(Path zipFile) throws IOException {
|
||||
this(zipFile, StandardCharsets.UTF_8);
|
||||
this(zipFile, false);
|
||||
}
|
||||
|
||||
public Zipper(Path zipFile, Charset encoding) throws IOException {
|
||||
this.zos = new ZipOutputStream(Files.newOutputStream(zipFile), encoding);
|
||||
public Zipper(Path zipFile, boolean allowDuplicateEntry) throws IOException {
|
||||
this.zos = new ZipOutputStream(Files.newOutputStream(zipFile), StandardCharsets.UTF_8);
|
||||
this.entryNames = allowDuplicateEntry ? new HashSet<>() : null;
|
||||
}
|
||||
|
||||
private static String normalize(String path) {
|
||||
@@ -57,6 +61,20 @@ public final class Zipper implements Closeable {
|
||||
return path;
|
||||
}
|
||||
|
||||
private ZipEntry newEntry(String name) throws IOException {
|
||||
if (entryNames == null || name.endsWith("/") || entryNames.add(name))
|
||||
return new ZipEntry(name);
|
||||
|
||||
for (int i = 1; i < 10; i++) {
|
||||
String newName = name + "." + i;
|
||||
if (entryNames.add(newName)) {
|
||||
return new ZipEntry(newName);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ZipException("duplicate entry: " + name);
|
||||
}
|
||||
|
||||
private static String resolve(String dir, String file) {
|
||||
if (dir.isEmpty()) return file;
|
||||
if (file.isEmpty()) return dir;
|
||||
@@ -71,7 +89,7 @@ public final class Zipper implements Closeable {
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param targetDir the path of the directory in this zip file.
|
||||
*/
|
||||
public void putDirectory(Path source, String targetDir) throws IOException {
|
||||
@@ -81,9 +99,9 @@ public final class Zipper implements Closeable {
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param targetDir the path of the directory in this zip file.
|
||||
* @param filter returns false if you do not want that file or directory
|
||||
* @param filter returns false if you do not want that file or directory
|
||||
*/
|
||||
public void putDirectory(Path source, String targetDir, ExceptionalPredicate<String, IOException> filter) throws IOException {
|
||||
String root = normalize(targetDir);
|
||||
@@ -118,16 +136,12 @@ public final class Zipper implements Closeable {
|
||||
});
|
||||
}
|
||||
|
||||
public void putFile(File file, String path) throws IOException {
|
||||
putFile(file.toPath(), path);
|
||||
}
|
||||
|
||||
public void putFile(Path file, String path) throws IOException {
|
||||
path = normalize(path);
|
||||
|
||||
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
|
||||
ZipEntry entry = new ZipEntry(attrs.isDirectory() ? path + "/" : path);
|
||||
ZipEntry entry = newEntry(attrs.isDirectory() ? path + "/" : path);
|
||||
entry.setCreationTime(attrs.creationTime());
|
||||
entry.setLastAccessTime(attrs.lastAccessTime());
|
||||
entry.setLastModifiedTime(attrs.lastModifiedTime());
|
||||
@@ -149,13 +163,13 @@ public final class Zipper implements Closeable {
|
||||
}
|
||||
|
||||
public void putStream(InputStream in, String path) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(normalize(path)));
|
||||
zos.putNextEntry(newEntry(normalize(path)));
|
||||
IOUtils.copyTo(in, zos, buffer);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
public OutputStream putStream(String path) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(normalize(path)));
|
||||
zos.putNextEntry(newEntry(normalize(path)));
|
||||
return new OutputStream() {
|
||||
public void write(int b) throws IOException {
|
||||
zos.write(b);
|
||||
@@ -180,7 +194,7 @@ public final class Zipper implements Closeable {
|
||||
}
|
||||
|
||||
public void putLines(Stream<String> lines, String path) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(normalize(path)));
|
||||
zos.putNextEntry(newEntry(normalize(path)));
|
||||
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos));
|
||||
@@ -205,7 +219,7 @@ public final class Zipper implements Closeable {
|
||||
}
|
||||
|
||||
public void putTextFile(String text, Charset encoding, String path) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(normalize(path)));
|
||||
zos.putNextEntry(newEntry(normalize(path)));
|
||||
zos.write(text.getBytes(encoding));
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
@@ -81,13 +81,8 @@ public final class SystemUtils {
|
||||
}
|
||||
|
||||
public static <T> T run(List<String> command, ExceptionalFunction<InputStream, T, ?> convert) throws Exception {
|
||||
File nul = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
? new File("NUL")
|
||||
: new File("/dev/null");
|
||||
|
||||
Process process = new ProcessBuilder(command)
|
||||
.redirectInput(nul)
|
||||
.redirectError(nul)
|
||||
.redirectError(ProcessBuilder.Redirect.DISCARD)
|
||||
.start();
|
||||
try {
|
||||
InputStream inputStream = process.getInputStream();
|
||||
|
||||
Reference in new issue
Block a user