Compare commits

..
Author SHA1 Message Date
Glavo 6cf757e440 Bump Shadow Plugin to 9.3.1 (#5392) 2026-02-02 20:53:59 +08:00
Glavo c0f4143618 优化 ComponentList (#5385) 2026-02-02 20:45:08 +08:00
Glavo 73b30974bf Bump XZ for Java to 1.11 (#5391) 2026-02-02 20:44:33 +08:00
Glavo 879f6cfeff Bump TwelveMonkeys to 3.13.0 (#5390) 2026-02-02 20:39:54 +08:00
Luo Chen 0fc8222e63 fix(ServerModpackCompletionTask): 修复已禁用模组仍被下载的问题 (#5333) 2026-02-02 20:35:23 +08:00
Xirrenandxirren 5c2bb1cc25 PS启动脚本使用LiteralPath避免歧义 (#5388)
close #5387

Co-authored-by: xirren <xirren@outlook.com>
2026-02-01 23:06:07 +08:00
Glavo c1d0bb973a 简化 Task (#5365) 2026-02-01 19:10:01 +08:00
Glavo c45cd74b67 简化 RipplerContainer (#5363) 2026-02-01 18:40:52 +08:00
辞庐 05251cb4e3 启动时检查 Jar 路径是否包含 ! (#5272) 2026-02-01 17:17:01 +08:00
NoClassDefFoundError ffbbde9d8f Fix #5037 #5307: 修复部分列表点击后无法通过 esc 返回的问题 (#5375) 2026-02-01 16:22:26 +08:00
Damon Lu c75bcd97b2 docs: 在贡献文档中添加专门针对 macOS 的 JDK 版本检查命令 (#5165) 2026-02-01 16:19:33 +08:00
NoClassDefFoundError 0cc0b21d5f Fix #5340: 修复不透明度数值标签可能被折叠的问题 (#5374) 2026-02-01 16:12:02 +08:00
Glavo a524ba8506 清理 EventManager (#5381) 2026-02-01 16:09:31 +08:00
Glavo 17be25c036 在 Linux 平台使用 dbus-send 检测深色模式设置 (#5317) 2026-02-01 16:05:19 +08:00
Dime 5a518cec03 fix: 修复版本设置页面滚动条总是显示的问题 (#5379)
Fix 5377
2026-02-01 15:15:35 +08:00
Glavo 0adf4fcf32 在 NBTTreeView 上启用平滑滚动 (#5364) 2026-02-01 15:11:10 +08:00
Glavo c4ed6b065b 将 PseudoClass 字段声明为静态字段 (#5362) 2026-01-30 20:59:00 +08:00
Glavo 0c4805df21 使用 LinePane 简化代码 (#5360) 2026-01-30 00:04:15 +08:00
27972b987e feat: 优化世界管理与世界信息页面 (#5215)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-01-29 22:39:47 +08:00
Chen Meng fe11e5ff1e 调整渲染器名称 (#4975) 2026-01-29 21:02:08 +08:00
Glavo e3231859ea 使用 LineSelectButton 代替 JFXComboxBox (#5337) 2026-01-29 20:28:45 +08:00
Glavo 90a795b2c8 在陶瓦联机页面添加切换账号菜单 (#5270) 2026-01-27 21:18:30 +08:00
辞庐 46f7e4db66 扫描子目录时忽略 .connector 文件夹 (#5296) 2026-01-27 21:13:34 +08:00
辞庐 d956c04f2a Fix #4763 : JFXTabPane 指示器偏移 (#5308) 2026-01-27 21:04:06 +08:00
辞庐 59e147227d Fix #5293 账户列表 > 添加认证服务器,非 CJK 语言下输入地址后下一步的对话框,超级宽…… (#5299) 2026-01-27 20:56:28 +08:00
辞庐 4cbe874c81 修复下载页面模组简介只会显示一行的问题 (#5300) 2026-01-27 20:52:34 +08:00
辞庐 e9e6ef1f20 fix: 修复 JFXPopup 在 RTL 下漂移的问题 (#5328) 2026-01-27 20:42:48 +08:00
Glavo d2ae41c9ae Fix #5280: 修复部分列表页面出现横向滚动条的问题 (#5284) 2026-01-27 20:40:07 +08:00
辞庐 79a1c3af8a fix(SystemUtils) : 移除 redirectInput(ProcessBuilder.Redirect.DISCARD) (#5325) 2026-01-25 15:30:45 +08:00
辞庐 c06f898d39 将 JFXTextField 右键菜单改为 JFX 样式 (#5292) 2026-01-25 15:28:47 +08:00
3gf8jv4dv b09b915d16 chore: update issue template [skip ci] (#5322) 2026-01-25 03:28:06 +08:00
Glavo 179ccb7805 在 Windows 平台上 GameCrashWindow 标题栏应当跟随深色模式设置 (#5319) 2026-01-25 03:27:39 +08:00
辞庐 f7987a91ce 取消 Windows 7 高版本的不兼容提示 (#5304) 2026-01-24 21:45:11 +08:00
辞庐 e68b96f67d Fix #4934: 返回页面时可能卡进空白页面 (#5305) 2026-01-24 21:41:04 +08:00
辞庐 7f6379960b Fix #5291: 点击实例列表条目的中央区域无法触发 Rippler 效果 (#5310) 2026-01-24 21:19:10 +08:00
Glavo 317b92c31e 简化 SystemUtils (#5315) 2026-01-24 20:44:25 +08:00
Glavo 35e65921f9 优化深色模式支持 (#5283) 2026-01-23 22:43:58 +08:00
Glavo 95eeae4c42 导出日志时允许重名文件 (#4733) 2026-01-23 22:41:07 +08:00
Glavo 165d77df30 重构 FutureCallback (#5268) 2026-01-23 22:19:43 +08:00
Glavo 65a4b6159d 优化更新内存信息任务 (#5286) 2026-01-23 22:18:59 +08:00
辞庐 6f690d2522 修复主页公告阴影被裁切的问题 (#5245) 2026-01-23 22:14:37 +08:00
Glavo 5a34ac0d2b 更新下载页面条目样式 (#5279) 2026-01-23 21:04:43 +08:00
Glavo 0ccb352b7e 清理 ComponentListCell (#5278) 2026-01-22 22:26:42 +08:00
Glavo 6d8add6d54 修复 Unzipper 解压空 zip 文件时抛出异常的问题 (#5267) 2026-01-21 22:07:06 +08:00
Glavo 7ab22b0e07 支持右键点击侧边栏弹出切换实例菜单 (#5269) 2026-01-21 22:05:32 +08:00
Glavo 20249ec4bf 重构 SchematicsPage 以使用 ListCell 展示元素 (#5276) 2026-01-21 22:04:45 +08:00
神奇的赵二 (OrzMiku) db38b4d532 修复自动安装界面卡片上按钮无法点击的问题 (#5251) 2026-01-21 22:04:11 +08:00
Glavo ba5e63395f refactor: update CommandBuilder methods to use addAll for multiple arguments (#5275) 2026-01-21 21:26:12 +08:00
Glavo 55cbb6cd2c 对 Java 26 默认启用压缩对象头 (#5274) 2026-01-21 20:51:06 +08:00
Glavo 8aecbe89f9 优化游戏代理参数设置 (#5273) 2026-01-21 20:40:28 +08:00
Xirrenandxirren 0a0476b6d3 修复系统代理 JVM 参数格式 (#5265)
Co-authored-by: xirren <xirren@outlook.com>
2026-01-20 21:57:25 +08:00
辞庐andGlavo 38612e5d6c 提早非法路径检查 (#5259)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-01-20 21:50:34 +08:00
mineDiamond 66e75fdc9d refactor: 移除 ListPage 和 ListPageSkin (#5244) 2026-01-19 21:46:53 +08:00
神奇的赵二 (OrzMiku) 1e76733564 优化实例列表,限制宽度防止出现水平滚动条 (#5243) 2026-01-19 21:31:25 +08:00
Wulian233 370d0ce78b Forge Quilt 模组管理支持识别子文件夹 (#5196)
Close #2178 
Close #5164
2026-01-19 21:27:24 +08:00
Glavo 6f402cd218 Fix #5256: 修复使用 GIF 背景时调整背景不透明度导致启动器崩溃的问题 (#5262) 2026-01-19 21:21:23 +08:00
Calboot e778d223a7 Modrinth 默认不获取更新日志 (#5250) 2026-01-19 21:09:59 +08:00
Glavo db5aa0c869 修复 GameVersionNumber.getDefaultGameVersions() 包含未混淆版本的问题 (#5261) 2026-01-19 20:38:51 +08:00
辞庐 8077b0bae1 feat: Legacy Fabric 自动安装 (#5090) 2026-01-19 20:35:56 +08:00
辞庐 e8c6c07e6a 修复 Cleanroom 本地安装完成后会删除安装器的问题 (#5257) 2026-01-19 20:27:14 +08:00
c1396c05aa 优化 Fabric / Quilt API 安装提示 (#4792)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 21:01:24 +08:00
Burning_TNT 9ca95c7f17 Fix #4958: 模组检查更新 (#5029) 2026-01-16 20:55:05 +08:00
neveler fb2d53664b 支持通过拖动标题栏退出最大化 (#5172)
close #5138
2026-01-16 20:33:20 +08:00
辞庐andGlavo 6fd6022210 导出带启动器 Modrinth 整合包使用 zip 格式 & 内部改为 mrpack (#5195)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-01-15 23:53:07 +08:00
mineDiamond 83782f5628 feat: 移动并简化 VersionPage.wrap 方法 (#5218) 2026-01-15 22:00:33 +08:00
Wulian233 a19f147a6a 将 GameJavaVersion 转换为 record (#5222) 2026-01-15 21:39:33 +08:00
Glavo 85ce3d9a7a RemoteImageLoader 在图标尚未加载时应当将图像更新为占位符 (#5239) 2026-01-15 20:41:18 +08:00
Glavo 886a1f1207 将世界管理界面“生成启动脚本”按钮折叠到二级菜单 (#5198) 2026-01-15 20:39:35 +08:00
Glavo 8d6245eb38 Bump version root to 3.11 (#5238) 2026-01-15 20:33:33 +08:00
150 changed files with 6885 additions and 2613 deletions

No files matched your search

+4 -4
View File
@@ -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(node,
parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0),
parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0)
);
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
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
@@ -50,10 +50,12 @@ public final class EntryPoint {
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
setupJavaFXVMOptions();
checkDirectoryPath();
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();
@@ -192,16 +194,6 @@ public final class EntryPoint {
}
}
private static void checkDirectoryPath() {
String currentDir = System.getProperty("user.dir", "");
if (currentDir.contains("!")) {
LOG.error("The current working path contains an exclamation mark: " + currentDir);
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDir);
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
@@ -253,7 +253,7 @@ public final class Launcher extends Application {
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
@@ -46,6 +46,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@@ -321,6 +322,8 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return VersionIconType.FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC))
return VersionIconType.LEGACY_FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
@@ -419,6 +422,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setWrapper(vs.getWrapper())
.setProxyOption(getProxyOption())
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
@@ -438,17 +442,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
}
if (config().hasProxy()) {
builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
Path json = getModpackConfiguration(version);
if (Files.exists(json)) {
try {
@@ -558,4 +551,39 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return minimum;
}
}
public static ProxyOption getProxyOption() {
if (!config().hasProxy() || config().getProxyType() == null) {
return ProxyOption.Default.INSTANCE;
}
return switch (config().getProxyType()) {
case DIRECT -> ProxyOption.Direct.INSTANCE;
case HTTP, SOCKS -> {
String proxyHost = config().getProxyHost();
int proxyPort = config().getProxyPort();
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
yield ProxyOption.Default.INSTANCE;
}
String proxyUser = config().getProxyUser();
String proxyPass = config().getProxyPass();
if (StringUtils.isBlank(proxyUser)) {
proxyUser = null;
proxyPass = null;
} else if (proxyPass == null) {
proxyPass = "";
}
if (config().getProxyType() == Proxy.Type.HTTP) {
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
} else {
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
}
}
default -> ProxyOption.Default.INSTANCE;
};
}
}
@@ -325,13 +325,13 @@ public final class LauncherHelper {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, 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")))
@@ -389,7 +389,7 @@ public final class LauncherHelper {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
@@ -634,7 +634,7 @@ public final class LauncherHelper {
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.yesOrNo(() -> {
@@ -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);
@@ -30,7 +30,9 @@ import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -57,7 +59,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
@Override
@@ -66,7 +68,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
public boolean isInstalled(Platform platform, String name) {
@@ -74,7 +76,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
@@ -94,7 +96,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
@@ -169,7 +171,7 @@ public final class HMCLJavaRepository implements JavaRepository {
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.getComponent());
update.put("component", gameJavaVersion.component());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
@@ -36,7 +36,9 @@ public enum VersionIconType {
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
CLEANROOM("/assets/img/cleanroom.png");
CLEANROOM("/assets/img/cleanroom.png"),
LEGACY_FABRIC("/assets/img/legacyfabric.png")
;
// Please append new items at last
@@ -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,13 +36,23 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class Themes {
@@ -65,7 +79,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 +111,66 @@ public final class Themes {
theme.addListener(listener);
}
private static Brightness defaultBrightness;
private static Brightness getDefaultBrightness() {
if (defaultBrightness != null)
return defaultBrightness;
LOG.info("Detecting system theme brightness");
Brightness brightness = Brightness.DEFAULT;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
WinReg reg = WinReg.INSTANCE;
if (reg != null) {
Object appsUseLightTheme = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "AppsUseLightTheme");
if (appsUseLightTheme instanceof Integer value) {
brightness = value == 0 ? Brightness.DARK : Brightness.LIGHT;
}
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
try {
String result = SystemUtils.run("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle").trim();
brightness = "Dark".equalsIgnoreCase(result) ? Brightness.DARK : Brightness.LIGHT;
} catch (Exception e) {
// If the key does not exist, it means Light mode is used
brightness = Brightness.LIGHT;
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
Path dbusSend = SystemUtils.which("dbus-send");
if (dbusSend != null) {
try {
String[] result = SystemUtils.run(List.of(
FileUtils.getAbsolutePath(dbusSend),
"--session",
"--print-reply=literal",
"--reply-timeout=1000",
"--dest=org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings.Read",
"string:org.freedesktop.appearance",
"string:color-scheme"
), Duration.ofSeconds(2)).trim().split(" ");
if (result.length > 0) {
String value = result[result.length - 1];
// 1: prefer dark
// 2: prefer light
if ("1".equals(value)) {
brightness = Brightness.DARK;
} else if ("2".equals(value)) {
brightness = Brightness.LIGHT;
}
}
} catch (Exception e) {
LOG.warning("Failed to get system theme from D-Bus", e);
}
}
}
LOG.info("Detected system theme brightness: " + brightness);
return defaultBrightness = brightness;
}
public static ObjectExpression<Theme> themeProperty() {
return theme;
}
@@ -129,6 +203,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() {
@@ -299,6 +312,14 @@ public final class FXUtils {
});
}
public static Node wrap(Node node) {
return limitingSize(node, 30, 20);
}
public static Node wrap(SVG svg) {
return wrap(svg.createIcon(20));
}
private static class ListenerPair<T> {
private final ObservableValue<T> value;
private final ChangeListener<? super T> listener;
@@ -412,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);
@@ -1264,8 +1299,8 @@ public final class FXUtils {
return button;
}
public static Label newSafeTruncatedLabel(String text) {
Label label = new Label(text);
public static Label newSafeTruncatedLabel() {
Label label = new Label();
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
showTooltipWhenTruncated(label);
return label;
@@ -1397,6 +1432,15 @@ public final class FXUtils {
});
}
public static void onSecondaryButtonClicked(Node node, Runnable action) {
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.SECONDARY) {
action.run();
e.consume();
}
});
}
public static <N extends Parent> N prepareNode(N node) {
Scene dummyScene = new Scene(node);
StyleSheets.init(dummyScene);
@@ -1592,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;
@@ -92,37 +92,18 @@ public class InstallerItem extends Control {
this.id = id;
this.style = style;
switch (id) {
case "game":
iconType = VersionIconType.GRASS;
break;
case "fabric":
case "fabric-api":
iconType = VersionIconType.FABRIC;
break;
case "forge":
iconType = VersionIconType.FORGE;
break;
case "cleanroom":
iconType = VersionIconType.CLEANROOM;
break;
case "liteloader":
iconType = VersionIconType.CHICKEN;
break;
case "optifine":
iconType = VersionIconType.OPTIFINE;
break;
case "quilt":
case "quilt-api":
iconType = VersionIconType.QUILT;
break;
case "neoforge":
iconType = VersionIconType.NEO_FORGE;
break;
default:
iconType = null;
break;
}
iconType = switch (id) {
case "game" -> VersionIconType.GRASS;
case "fabric", "fabric-api" -> VersionIconType.FABRIC;
case "legacyfabric", "legacyfabric-api" -> VersionIconType.LEGACY_FABRIC;
case "forge" -> VersionIconType.FORGE;
case "cleanroom" -> VersionIconType.CLEANROOM;
case "liteloader" -> VersionIconType.CHICKEN;
case "optifine" -> VersionIconType.OPTIFINE;
case "quilt", "quilt-api" -> VersionIconType.QUILT;
case "neoforge" -> VersionIconType.NEO_FORGE;
default -> null;
};
}
public String getLibraryId() {
@@ -201,6 +182,8 @@ public class InstallerItem extends Control {
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
InstallerItem forge = new InstallerItem(FORGE, style);
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
InstallerItem legacyfabric = new InstallerItem(LEGACY_FABRIC, style);
InstallerItem legacyfabricApi = new InstallerItem(LEGACY_FABRIC_API, style);
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
@@ -208,11 +191,11 @@ public class InstallerItem extends Control {
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom);
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom, legacyfabric);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom, liteLoader, legacyfabric);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabricApi, legacyfabricApi);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
@@ -246,7 +229,7 @@ public class InstallerItem extends Control {
game.versionProperty.set(new InstalledState(gameVersion, false, false));
}
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, cleanroom};
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, legacyfabric, legacyfabricApi, cleanroom};
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
@@ -263,9 +246,9 @@ public class InstallerItem extends Control {
if (gameVersion == null) {
this.libraries = all;
} else if (gameVersion.equals("1.12.2")) {
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, optiFine};
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, legacyfabric, legacyfabricApi, optiFine};
} else if (GameVersionNumber.compare(gameVersion, "1.13.2") <= 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine, legacyfabric, legacyfabricApi};
} else {
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
}
@@ -1,49 +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;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends ListPageBase<T> {
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
public abstract void add();
public void refresh() {
}
@Override
protected Skin<?> createDefaultSkin() {
return new ListPageSkin(this);
}
public boolean isRefreshable() {
return refreshable.get();
}
public BooleanProperty refreshableProperty() {
return refreshable;
}
public void setRefreshable(boolean refreshable) {
this.refreshable.set(refreshable);
}
}
@@ -1,108 +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;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
public class ListPageSkin extends SkinBase<ListPage<?>> {
public ListPageSkin(ListPage<?> skinnable) {
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
StackPane contentPane = new StackPane();
{
ScrollPane scrollPane = new ScrollPane();
{
scrollPane.setFitToWidth(true);
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
FXUtils.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
{
vBox.getStyleClass().add("card-list");
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
JFXButton btnAdd = FXUtils.newRaisedButton("");
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.setGraphic(SVG.ADD.createIcon());
btnAdd.setOnAction(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
refreshable -> {
if (refreshable) {
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnRefresh, btnAdd);
} else {
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnAdd);
}
});
}
// Keep a blank space to prevent buttons from blocking up mod items.
BorderPane group = new BorderPane();
group.setPickOnBounds(false);
group.setBottom(vBox);
placeholder.minHeightProperty().bind(vBox.heightProperty());
contentPane.getChildren().setAll(scrollPane, group);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(contentPane);
getChildren().setAll(spinnerPane);
}
}
@@ -21,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"),
@@ -120,7 +123,9 @@ public enum SVG {
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UNFOLD_MORE("M12 21 7.5 16.5l1.45-1.45L12 18.1l3.05-3.05 1.45 1.45L12 21ZM8.95 9.05 7.5 7.6 12 3.1l4.5 4.5-1.45 1.45L12 6 8.95 9.05Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
UNDO("M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
@@ -25,6 +25,8 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
@@ -65,6 +67,7 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
ComponentList.setVgrow(listView, Priority.ALWAYS);
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
root.getContent().add(listView);
}
@@ -54,7 +54,7 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -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(
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import java.util.function.Consumer;
@@ -75,7 +74,7 @@ public class AdvancedListBox extends ScrollPane {
item.setActionButtonVisible(false);
item.setTitle(title);
if (leftGraphic != null) {
item.setLeftGraphic(VersionPage.wrap(leftGraphic));
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
}
return item;
}
@@ -25,7 +25,7 @@ import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public AdvancedListItemSkin(AdvancedListItem skinnable) {
super(skinnable);
@@ -17,13 +17,8 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
@@ -33,88 +28,21 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.List;
import java.util.function.Supplier;
@DefaultProperty("content")
public class ComponentList extends Control {
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle", "");
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
private boolean hasSubtitle = false;
public final ObservableList<Node> content = FXCollections.observableArrayList();
private Supplier<List<? extends Node>> lazyInitializer;
public class ComponentList extends Control implements NoPaddingComponent {
public ComponentList() {
getStyleClass().add("options-list");
}
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
this();
this.lazyInitializer = lazyInitializer;
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public int getDepth() {
return depth.get();
}
public IntegerProperty depthProperty() {
return depth;
}
public void setDepth(int depth) {
this.depth.set(depth);
}
public boolean isHasSubtitle() {
return hasSubtitle;
}
public void setHasSubtitle(boolean hasSubtitle) {
this.hasSubtitle = hasSubtitle;
}
private final ObservableList<Node> content = FXCollections.observableArrayList();
public ObservableList<Node> getContent() {
return content;
}
void doLazyInit() {
if (lazyInitializer != null) {
this.getContent().setAll(lazyInitializer.get());
setNeedsLayout(true);
lazyInitializer = null;
}
}
@Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
@@ -130,49 +58,72 @@ public class ComponentList extends Control {
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
private final ObjectBinding<Node> lastItem;
Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), node -> {
ComponentListCell cell = new ComponentListCell(node);
cell.getStyleClass().add("options-list-item");
if (node.getProperties().containsKey("ComponentList.vgrow")) {
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
Pane wrapper;
if (node instanceof ComponentSublist sublist) {
sublist.getStyleClass().remove("options-list");
sublist.getStyleClass().add("options-sublist");
wrapper = new ComponentSublistWrapper(sublist);
} else {
wrapper = new StackPane(node);
}
if (node.getProperties().containsKey("ComponentList.noPadding")) {
cell.getStyleClass().add("no-padding");
wrapper.getStyleClass().add("options-list-item");
if (node.getProperties().get("ComponentList.vgrow") instanceof Priority priority) {
VBox.setVgrow(wrapper, priority);
}
return cell;
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding")) {
wrapper.getStyleClass().add("no-padding");
}
return wrapper;
});
firstItem = Bindings.valueAt(list, 0);
firstItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
});
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
lastItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
});
if (!list.isEmpty())
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
updateStyle();
list.addListener((InvalidationListener) o -> updateStyle());
VBox vbox = new VBox();
vbox.setFillWidth(true);
Bindings.bindContent(vbox.getChildren(), list);
node = vbox;
}
private Node prevFirstItem;
private Node prevLastItem;
private void updateStyle() {
Node firstItem;
Node lastItem;
if (list.isEmpty()) {
firstItem = null;
lastItem = null;
} else {
firstItem = list.get(0);
lastItem = list.get(list.size() - 1);
}
if (firstItem != prevFirstItem) {
if (prevFirstItem != null)
prevFirstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
if (firstItem != null)
firstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
prevFirstItem = firstItem;
}
if (lastItem != prevLastItem) {
if (prevLastItem != null)
prevLastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
if (lastItem != null)
lastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
prevLastItem = lastItem;
}
}
}
public static Node createComponentListTitle(String title) {
@@ -189,4 +140,8 @@ public class ComponentList extends Control {
public static void setVgrow(Node node, Priority priority) {
node.getProperties().put("ComponentList.vgrow", priority);
}
public static void setNoPadding(Node node) {
node.getProperties().put("ComponentList.noPadding", true);
}
}
@@ -1,215 +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 javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
/**
* @author huangyuhui
*/
final class ComponentListCell extends StackPane {
private final Node content;
private Animation expandAnimation;
private Rectangle clipRect;
private final BooleanProperty expanded = new SimpleBooleanProperty(this, "expanded", false);
ComponentListCell(Node content) {
this.content = content;
updateLayout();
}
private void updateClip(double newHeight) {
if (clipRect != null)
clipRect.setHeight(newHeight);
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (clipRect == null)
clipRect = new Rectangle(0, 0, getWidth(), getHeight());
else {
clipRect.setX(0);
clipRect.setY(0);
clipRect.setHeight(getHeight());
clipRect.setWidth(getWidth());
}
}
private void updateLayout() {
if (content instanceof ComponentList list) {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
getStyleClass().add("no-padding");
VBox groupNode = new VBox();
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
expandIcon.setMouseTransparent(true);
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
VBox labelVBox = new VBox();
labelVBox.setMouseTransparent(true);
labelVBox.setAlignment(Pos.CENTER_LEFT);
boolean overrideHeaderLeft = false;
if (list instanceof ComponentSublist) {
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
if (leftNode != null) {
labelVBox.getChildren().setAll(leftNode);
overrideHeaderLeft = true;
}
}
if (!overrideHeaderLeft) {
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.getStyleClass().add("title-label");
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.getStyleClass().add("subtitle-label");
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
labelVBox.getChildren().add(subtitleLabel);
}
}
HBox header = new HBox();
header.setSpacing(16);
header.getChildren().add(labelVBox);
header.setPadding(new Insets(10, 16, 10, 16));
header.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(labelVBox, Priority.ALWAYS);
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
header.getChildren().add(rightNode);
}
header.getChildren().add(expandIcon);
RipplerContainer headerRippler = new RipplerContainer(header);
groupNode.getChildren().add(headerRippler);
VBox container = new VBox();
boolean hasPadding = !(list instanceof ComponentSublist subList) || subList.hasComponentPadding();
if (hasPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(content);
groupNode.getChildren().add(container);
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
event.consume();
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
boolean expanded = !isExpanded();
setExpanded(expanded);
if (expanded) {
list.doLazyInit();
list.layout();
}
Platform.runLater(() -> {
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 4)) * (expanded ? 1 : -1);
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(list.getWidth());
double contentHeight = expanded ? newAnimatedHeight : 0;
double targetRotate = expanded ? -180 : 0;
if (expanded) {
updateClip(newHeight);
}
if (AnimationUtils.isAnimationEnabled()) {
double currentRotate = expandIcon.getRotate();
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
expandAnimation = new Timeline(
new KeyFrame(duration,
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
);
if (!expanded) {
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
}
expandAnimation.play();
} else {
container.setMinHeight(contentHeight);
container.setMaxHeight(contentHeight);
expandIcon.setRotate(targetRotate);
if (!expanded) {
updateClip(newHeight);
}
}
});
});
getChildren().setAll(groupNode);
} else {
getStyleClass().remove("no-padding");
getChildren().setAll(content);
}
}
public boolean isExpanded() {
return expanded.get();
}
public BooleanProperty expandedProperty() {
return expanded;
}
public void setExpanded(boolean expanded) {
this.expanded.set(expanded);
}
}
@@ -17,57 +17,100 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.scene.Node;
@DefaultProperty("content")
import java.util.List;
import java.util.function.Supplier;
public class ComponentSublist extends ComponentList {
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
Supplier<List<? extends Node>> lazyInitializer;
public ComponentSublist() {
super();
}
public Node getHeaderLeft() {
return headerLeft.get();
public ComponentSublist(Supplier<List<? extends Node>> lazyInitializer) {
this.lazyInitializer = lazyInitializer;
}
public ObjectProperty<Node> headerLeftProperty() {
void doLazyInit() {
if (lazyInitializer != null) {
this.getContent().setAll(lazyInitializer.get());
setNeedsLayout(true);
lazyInitializer = null;
}
}
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
titleProperty().set(title);
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null)
subtitle = new SimpleStringProperty(this, "subtitle", "");
return subtitle;
}
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
private boolean hasSubtitle = false;
public boolean isHasSubtitle() {
return hasSubtitle;
}
public void setHasSubtitle(boolean hasSubtitle) {
this.hasSubtitle = hasSubtitle;
}
private Node headerLeft;
public Node getHeaderLeft() {
return headerLeft;
}
public void setHeaderLeft(Node headerLeft) {
this.headerLeft.set(headerLeft);
this.headerLeft = headerLeft;
}
private Node headerRight;
public Node getHeaderRight() {
return headerRight.get();
}
public ObjectProperty<Node> headerRightProperty() {
return headerRight;
}
public void setHeaderRight(Node headerRight) {
this.headerRight.set(headerRight);
this.headerRight = headerRight;
}
private boolean componentPadding = true;
public boolean hasComponentPadding() {
return componentPadding.get();
}
public BooleanProperty componentPaddingProperty() {
return componentPadding;
}
public void setComponentPadding(boolean componentPadding) {
this.componentPadding.set(componentPadding);
this.componentPadding = componentPadding;
}
}
@@ -0,0 +1,146 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
/// @author Glavo
final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
private VBox container;
private Animation expandAnimation;
private boolean expanded = false;
ComponentSublistWrapper(ComponentSublist sublist) {
boolean noPadding = !sublist.hasComponentPadding();
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
expandIcon.setMouseTransparent(true);
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
VBox labelVBox = new VBox();
labelVBox.setMouseTransparent(true);
labelVBox.setAlignment(Pos.CENTER_LEFT);
Node leftNode = sublist.getHeaderLeft();
if (leftNode == null) {
Label label = new Label();
label.textProperty().bind(sublist.titleProperty());
label.getStyleClass().add("title-label");
labelVBox.getChildren().add(label);
if (sublist.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(sublist.subtitleProperty());
subtitleLabel.getStyleClass().add("subtitle-label");
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
labelVBox.getChildren().add(subtitleLabel);
}
} else {
labelVBox.getChildren().setAll(leftNode);
}
HBox header = new HBox();
header.setSpacing(16);
header.getChildren().add(labelVBox);
header.setPadding(new Insets(10, 16, 10, 16));
header.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(labelVBox, Priority.ALWAYS);
Node rightNode = sublist.getHeaderRight();
if (rightNode != null)
header.getChildren().add(rightNode);
header.getChildren().add(expandIcon);
RipplerContainer headerRippler = new RipplerContainer(header);
this.getChildren().add(headerRippler);
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
event.consume();
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
boolean expanded = !this.expanded;
this.expanded = expanded;
if (expanded) {
sublist.doLazyInit();
if (container == null) {
this.container = new VBox();
if (!noPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(sublist);
ComponentSublistWrapper.this.getChildren().add(container);
this.applyCss();
}
this.layout();
}
Platform.runLater(() -> {
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
double targetRotate = expanded ? -180 : 0;
if (AnimationUtils.isAnimationEnabled()) {
double currentRotate = expandIcon.getRotate();
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
expandAnimation = new Timeline(
new KeyFrame(duration,
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
);
expandAnimation.play();
} else {
container.setMinHeight(contentHeight);
container.setMaxHeight(contentHeight);
expandIcon.setRotate(targetRotate);
}
});
});
}
}
@@ -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());
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
/// @author Glavo
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent {
private static final Insets PADDING = new Insets(8, 8, 8, 16);
protected final BorderPane root;
protected final RipplerContainer container;
private final Label titleLabel;
public LineButtonBase() {
this.root = new BorderPane();
root.setPadding(PADDING);
root.setMinHeight(48);
this.container = new RipplerContainer(root);
this.getChildren().setAll(container);
this.titleLabel = new Label();
root.setCenter(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.textProperty().bind(titleProperty());
titleLabel.getStyleClass().add("title");
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
private VBox left;
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
@Override
public Object getBean() {
return LineButtonBase.this;
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
root.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
root.setCenter(titleLabel);
}
}
};
}
return subtitle;
}
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
}
@@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public final class LineNavigationButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-navigation-button";
public LineNavigationButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
HBox right = new HBox();
root.setRight(right);
{
right.setAlignment(Pos.CENTER_RIGHT);
Label valueLabel = new Label();
valueLabel.getStyleClass().add("subtitle");
valueLabel.textProperty().bind(messageProperty());
Node arrowIcon = SVG.ARROW_FORWARD.createIcon(24);
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
disabledProperty().addListener((observable, oldValue, newValue) ->
arrowIcon.setOpacity(newValue ? 0.4 : 1.0));
right.getChildren().setAll(valueLabel, arrowIcon);
}
FXUtils.onClicked(container, this::fire);
}
public void fire() {
fireEvent(new ActionEvent());
}
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
@Override
public Object getBean() {
return LineNavigationButton.this;
}
@Override
public String getName() {
return "onAction";
}
};
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
public EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
public void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
private final StringProperty message = new SimpleStringProperty(this, "message", "");
public StringProperty messageProperty() {
return message;
}
public String getMessage() {
return messageProperty().get();
}
public void setMessage(String message) {
messageProperty().set(message);
}
}
@@ -0,0 +1,115 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
/// @author Glavo
public class LinePane extends BorderPane implements NoPaddingComponent {
private static final Insets PADDING = new Insets(8, 8, 8, 16);
private final Label titleLabel;
public LinePane() {
this.setPadding(PADDING);
this.setMinHeight(48);
this.titleLabel = new Label();
this.setCenter(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.textProperty().bind(titleProperty());
titleLabel.getStyleClass().add("title");
}
private final StringProperty title = new SimpleStringProperty(this, "title");
public StringProperty titleProperty() {
return title;
}
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle;
public StringProperty subtitleProperty() {
if (subtitle == null) {
subtitle = new StringPropertyBase() {
private VBox left;
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
@Override
public Object getBean() {
return LinePane.this;
}
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
LinePane.this.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
LinePane.this.setCenter(titleLabel);
}
}
};
}
return subtitle;
}
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
}
@@ -0,0 +1,231 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public final class LineSelectButton<T> extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
private JFXPopup popup;
public LineSelectButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
HBox right = new HBox();
root.setRight(right);
{
right.setAlignment(Pos.CENTER_RIGHT);
Label valueLabel = new Label();
valueLabel.getStyleClass().add("subtitle");
InvalidationListener updateValue = observable -> {
T value = getValue();
if (value == null)
valueLabel.setText("");
else {
Function<T, String> converter = getConverter();
valueLabel.setText(converter != null ? converter.apply(value) : value.toString());
}
};
converterProperty().addListener(updateValue);
valueProperty().addListener(updateValue);
Node arrowIcon = SVG.UNFOLD_MORE.createIcon(24);
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
right.getChildren().setAll(valueLabel, arrowIcon);
}
container.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
if (popup == null) {
PopupMenu popupMenu = new PopupMenu();
this.popup = new JFXPopup(popupMenu);
container.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
itemTitleLabel.getStyleClass().add("title");
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (item == null)
return "";
Function<T, String> converter = getConverter();
return converter != null ? converter.apply(item) : Objects.toString(item, "");
}, converterProperty()));
var itemSubtitleLabel = new Label();
itemSubtitleLabel.getStyleClass().add("subtitle");
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
Function<T, String> descriptionConverter = getDescriptionConverter();
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
}, descriptionConverterProperty()));
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
if (text == null || text.isEmpty()) {
vbox.getChildren().setAll(itemTitleLabel);
} else {
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
}
});
var wrapper = new StackPane(vbox);
wrapper.setAlignment(Pos.CENTER_LEFT);
wrapper.getStyleClass().add("menu-container");
wrapper.setMouseTransparent(true);
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
FXUtils.onClicked(ripplerContainer, () -> {
setValue(item);
popup.hide();
});
FXUtils.onChangeAndOperate(valueProperty(),
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
return ripplerContainer;
}));
popup.showingProperty().addListener((observable, oldValue, newValue) ->
container.getRippler().setRipplerDisabled(newValue));
}
if (popup.isShowing()) {
popup.hide();
} else {
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
0,
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight());
}
event.consume();
} else if (event.getButton() == MouseButton.SECONDARY) {
if (popup != null)
popup.hide();
event.consume();
}
});
}
private final ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
public ObjectProperty<T> valueProperty() {
return value;
}
public T getValue() {
return valueProperty().get();
}
public void setValue(T value) {
valueProperty().set(value);
}
private final ObjectProperty<Function<T, String>> converter = new SimpleObjectProperty<>(this, "converter");
public ObjectProperty<Function<T, String>> converterProperty() {
return converter;
}
public Function<T, String> getConverter() {
return converterProperty().get();
}
public void setConverter(Function<T, String> value) {
converterProperty().set(value);
}
private ObjectProperty<Function<T, String>> descriptionConverter;
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
if (descriptionConverter == null) {
descriptionConverter = new SimpleObjectProperty<>(this, "descriptionConverter");
}
return descriptionConverter;
}
public Function<T, String> getDescriptionConverter() {
return descriptionConverterProperty().get();
}
public void setDescriptionConverter(Function<T, String> value) {
descriptionConverterProperty().set(value);
}
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.emptyObservableList());
public ListProperty<T> itemsProperty() {
return items;
}
public void setItems(ObservableList<T> value) {
itemsProperty().set(value);
}
public void setItems(Collection<T> value) {
if (value instanceof ObservableList<T> observableList) {
this.setItems(observableList);
} else {
this.setItems(FXCollections.observableArrayList(value));
}
}
@SafeVarargs
public final void setItems(T... values) {
this.setItems(FXCollections.observableArrayList(values));
}
public ObservableList<T> getItems() {
return items.get();
}
}
@@ -0,0 +1,86 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
/// @author Glavo
public final class LineTextPane extends LinePane {
private static final String DEFAULT_STYLE_CLASS = "line-label-pane";
public LineTextPane() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
private StringProperty text;
public StringProperty textProperty() {
if (text == null) {
text = new StringPropertyBase() {
private static final Insets LABEL_MARGIN = new Insets(0, 8, 0, 16);
private Label rightLabel;
@Override
public Object getBean() {
return LineTextPane.this;
}
@Override
public String getName() {
return "text";
}
@Override
protected void invalidated() {
String text = get();
if (text == null || text.isEmpty()) {
if (rightLabel != null)
rightLabel.setText(null);
LineTextPane.this.setRight(null);
} else {
if (rightLabel == null) {
rightLabel = FXUtils.newSafeTruncatedLabel();
FXUtils.copyOnDoubleClick(rightLabel);
BorderPane.setMargin(rightLabel, LABEL_MARGIN);
BorderPane.setAlignment(rightLabel, Pos.CENTER_RIGHT);
}
rightLabel.setText(text);
LineTextPane.this.setRight(rightLabel);
}
}
};
}
return text;
}
public String getText() {
return textProperty().get();
}
public void setText(String text) {
textProperty().set(text);
}
}
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXToggleButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
public final class LineToggleButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-toggle-button";
public LineToggleButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
JFXToggleButton toggleButton = new JFXToggleButton();
toggleButton.selectedProperty().bindBidirectional(selectedProperty());
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
BorderPane.setAlignment(toggleButton, Pos.CENTER);
root.setRight(toggleButton);
FXUtils.onClicked(container, toggleButton::fire);
}
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
public BooleanProperty selectedProperty() {
return selected;
}
public boolean isSelected() {
return selectedProperty().get();
}
public void setSelected(boolean selected) {
selectedProperty().set(selected);
}
}
@@ -26,7 +26,7 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
public abstract class MDListCell<T> extends ListCell<T> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
@@ -1,164 +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.JFXColorPicker;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiColorItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final ToggleGroup group = new ToggleGroup();
private final JFXColorPicker colorPicker = new JFXColorPicker();
private final JFXRadioButton radioCustom = new JFXRadioButton();
private final BorderPane custom = new BorderPane();
private final VBox pane = new VBox();
private final boolean hasCustom;
private Consumer<Toggle> toggleSelectedListener;
private Consumer<Color> colorConsumer;
public MultiColorItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
radioCustom.textProperty().bind(customTitleProperty());
radioCustom.setToggleGroup(group);
colorPicker.disableProperty().bind(radioCustom.selectedProperty().not());
colorPicker.setOnAction(e -> Optional.ofNullable(colorConsumer).ifPresent(c -> c.accept(colorPicker.getValue())));
custom.setLeft(radioCustom);
custom.setStyle("-fx-padding: 3;");
HBox right = new HBox();
right.setSpacing(3);
right.getChildren().addAll(colorPicker);
custom.setRight(right);
FXUtils.setLimitHeight(custom, 40);
pane.setStyle("-fx-padding: 0 0 10 0;");
pane.setSpacing(8);
if (hasCustom)
pane.getChildren().add(custom);
getContent().add(pane);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
}
public Node createChildren(String title) {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
JFXRadioButton left = new JFXRadioButton(title);
left.setToggleGroup(group);
left.setUserData(userData);
pane.setLeft(left);
Label right = new Label(subtitle);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
return pane;
}
public void loadChildren(Collection<Node> list) {
pane.getChildren().setAll(list);
if (hasCustom)
pane.getChildren().add(custom);
}
public ToggleGroup getGroup() {
return group;
}
public String getCustomTitle() {
return customTitle.get();
}
public StringProperty customTitleProperty() {
return customTitle;
}
public void setCustomTitle(String customTitle) {
this.customTitle.set(customTitle);
}
public String getChooserTitle() {
return chooserTitle.get();
}
public StringProperty chooserTitleProperty() {
return chooserTitle;
}
public void setChooserTitle(String chooserTitle) {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
radioCustom.setUserData(userData);
}
public boolean isCustomToggle(Toggle toggle) {
return radioCustom == toggle;
}
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
toggleSelectedListener = consumer;
}
public void setOnColorPickerChanged(Consumer<Color> consumer) {
colorConsumer = consumer;
}
public void setColor(Color color) {
colorPicker.setValue(color);
}
}
@@ -0,0 +1,24 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
/// Marker interface for no padding in [ComponentList].
///
/// @author Glavo
interface NoPaddingComponent {
}
@@ -1,113 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXToggleButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
public class OptionToggleButton extends StackPane {
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final BooleanProperty selected = new SimpleBooleanProperty();
public OptionToggleButton() {
getProperties().put("ComponentList.noPadding", true);
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 8, 8, 16));
RipplerContainer container = new RipplerContainer(pane);
getChildren().setAll(container);
VBox left = new VBox();
left.setMouseTransparent(true);
Label titleLabel = new Label();
titleLabel.textProperty().bind(title);
Label subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle");
subtitleLabel.textProperty().bind(subtitle);
pane.setCenter(left);
left.setAlignment(Pos.CENTER_LEFT);
JFXToggleButton toggleButton = new JFXToggleButton();
StackPane right = new StackPane(toggleButton);
right.setAlignment(Pos.CENTER);
pane.setRight(right);
toggleButton.selectedProperty().bindBidirectional(selected);
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
left.getChildren().setAll(titleLabel, subtitleLabel);
} else {
left.getChildren().setAll(titleLabel);
}
});
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
}
@@ -0,0 +1,236 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.InsetsConverter;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
// TODO: We plan to replace ComponentList with this class, but we need to address some issues first
/// @author Glavo
public final class OptionsList extends Control {
public OptionsList() {
this.getStyleClass().add("options-list");
}
@Override
protected Skin<?> createDefaultSkin() {
return new OptionsListSkin(this);
}
private final ObservableList<Element> elements = FXCollections.observableArrayList();
public ObservableList<Element> getElements() {
return elements;
}
public void addTitle(String title) {
elements.add(new Title(title));
}
public void addNode(Node node) {
elements.add(new NodeElement(node));
}
public void addListElement(@NotNull Node node) {
elements.add(new ListElement(node));
}
public void addListElements(@NotNull Node... nodes) {
for (Node node : nodes) {
elements.add(new ListElement(node));
}
}
private final StyleableObjectProperty<Insets> contentPadding = new StyleableObjectProperty<>() {
@Override
public Object getBean() {
return OptionsList.this;
}
@Override
public String getName() {
return "contentPadding";
}
@Override
public javafx.css.CssMetaData<OptionsList, Insets> getCssMetaData() {
return StyleableProperties.CONTENT_PADDING;
}
};
public StyleableObjectProperty<Insets> contentPaddingProperty() {
return contentPadding;
}
public Insets getContentPadding() {
return contentPaddingProperty().get();
}
public void setContentPadding(Insets padding) {
contentPaddingProperty().set(padding);
}
private static final class StyleableProperties {
private static final CssMetaData<OptionsList, Insets> CONTENT_PADDING = new CssMetaData<>("-jfx-content-padding", InsetsConverter.getInstance()) {
@Override
public boolean isSettable(OptionsList styleable) {
return styleable.contentPadding == null || !styleable.contentPadding.isBound();
}
@Override
public StyleableProperty<Insets> getStyleableProperty(OptionsList styleable) {
return styleable.contentPaddingProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, CONTENT_PADDING);
STYLEABLES = List.copyOf(styleables);
}
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static abstract class Element {
protected Node node;
Node getNode() {
if (node == null)
node = createNode();
return node;
}
protected abstract Node createNode();
}
public static final class Title extends Element {
private final @NotNull String title;
public Title(@NotNull String title) {
this.title = title;
}
@Override
protected Node createNode() {
Label label = new Label(title);
label.setPadding(new Insets(8, 0, 8, 0));
return label;
}
@Override
public boolean equals(Object obj) {
return this == obj || obj instanceof Title that && Objects.equals(this.title, that.title);
}
@Override
public int hashCode() {
return title.hashCode();
}
@Override
public String toString() {
return "Title[%s]".formatted(title);
}
}
public static final class NodeElement extends Element {
public NodeElement(@NotNull Node node) {
this.node = node;
}
@Override
protected Node createNode() {
return node;
}
@Override
public boolean equals(Object obj) {
return this == obj || obj instanceof NodeElement that && this.node.equals(that.node);
}
@Override
public int hashCode() {
return node.hashCode();
}
@Override
public String toString() {
return "NodeElement[node=%s]".formatted(node);
}
}
public static final class ListElement extends Element {
private final Node original;
public ListElement(@NotNull Node node) {
this.original = node;
}
@Override
protected Node createNode() {
if (original instanceof ComponentSublist sublist) {
return new ComponentSublistWrapper(sublist);
} else {
return original;
}
}
@Override
public boolean equals(Object obj) {
return this == obj || obj instanceof ListElement that && this.original.equals(that.original);
}
@Override
public int hashCode() {
return original.hashCode();
}
@Override
public String toString() {
return "ListElement[node=%s]".formatted(original);
}
}
}
@@ -0,0 +1,152 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
/// @author Glavo
public final class OptionsListSkin extends SkinBase<OptionsList> {
private final JFXListView<OptionsList.Element> listView;
private final ObjectBinding<ContentPaddings> contentPaddings;
OptionsListSkin(OptionsList control) {
super(control);
this.listView = new JFXListView<>();
listView.setItems(control.getElements());
listView.setCellFactory(listView1 -> new Cell());
this.contentPaddings = Bindings.createObjectBinding(() -> {
Insets padding = control.getContentPadding();
return padding == null ? ContentPaddings.EMPTY : new ContentPaddings(
new Insets(padding.getTop(), padding.getRight(), 0, padding.getLeft()),
new Insets(0, padding.getRight(), padding.getBottom(), padding.getLeft()),
new Insets(0, padding.getRight(), 0, padding.getLeft())
);
}, control.contentPaddingProperty());
this.getChildren().setAll(listView);
}
private record ContentPaddings(Insets first, Insets last, Insets middle) {
static final ContentPaddings EMPTY = new ContentPaddings(Insets.EMPTY, Insets.EMPTY, Insets.EMPTY);
}
private final class Cell extends ListCell<OptionsList.Element> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateStyleListener = o -> updateStyle();
private StackPane wrapper;
public Cell() {
FXUtils.limitCellWidth(listView, this);
WeakInvalidationListener weakListener = new WeakInvalidationListener(updateStyleListener);
listView.itemsProperty().addListener((o, oldValue, newValue) -> {
if (oldValue != null)
oldValue.removeListener(weakListener);
if (newValue != null)
newValue.addListener(weakListener);
weakListener.invalidated(o);
});
itemProperty().addListener(weakListener);
contentPaddings.addListener(weakListener);
}
@Override
protected void updateItem(OptionsList.Element item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else if (item instanceof OptionsList.ListElement element) {
if (wrapper == null)
wrapper = createWrapper();
else
wrapper.getStyleClass().remove("no-padding");
Node node = element.getNode();
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding"))
wrapper.getStyleClass().add("no-padding");
wrapper.getChildren().setAll(node);
setGraphic(wrapper);
} else {
setGraphic(item.getNode());
}
updateStyle();
}
private StackPane createWrapper() {
var wrapper = new StackPane();
wrapper.setAlignment(Pos.CENTER_LEFT);
wrapper.getStyleClass().add("options-list-item");
updateStyle();
return wrapper;
}
private void updateStyle() {
OptionsList.Element item = getItem();
int index = getIndex();
ObservableList<OptionsList.Element> items = getListView().getItems();
if (item == null || index < 0 || index >= items.size()) {
this.setPadding(Insets.EMPTY);
return;
}
boolean isFirst = index == 0;
boolean isLast = index == items.size() - 1;
ContentPaddings paddings = contentPaddings.get();
if (isFirst) {
this.setPadding(paddings.first);
} else if (isLast) {
this.setPadding(paddings.last);
} else {
this.setPadding(paddings.middle);
}
if (item instanceof OptionsList.ListElement && wrapper != null) {
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, isFirst || !(items.get(index - 1) instanceof OptionsList.ListElement));
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_LAST, isLast || !(items.get(index + 1) instanceof OptionsList.ListElement));
}
}
}
}
@@ -78,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));
}
});
}
@@ -19,17 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.PaintConverter;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
@@ -37,39 +32,41 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.util.Lang;
import java.util.ArrayList;
import java.util.List;
@DefaultProperty("container")
public class RipplerContainer extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "rippler-container";
private static final Duration DURATION = Duration.millis(200);
private static final CornerRadii DEFAULT_RADII = new CornerRadii(3);
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null);
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL, this, "ripplerFill", null);
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
private final Node container;
private final StackPane buttonContainer = new StackPane();
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
private static final Background DEFAULT_MASK_BACKGROUND = new Background(new BackgroundFill(Color.WHITE, DEFAULT_RADII, Insets.EMPTY));
@Override
protected Node getMask() {
StackPane mask = new StackPane();
mask.shapeProperty().bind(buttonContainer.shapeProperty());
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(new BackgroundFill(Color.WHITE, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getRadii() : defaultRadii, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)), buttonContainer.backgroundProperty()));
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
mask.setBackground(DEFAULT_MASK_BACKGROUND);
mask.resize(
buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(),
buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset()
);
return mask;
}
};
private final CornerRadii defaultRadii = new CornerRadii(3);
private Transition coverAnimation;
public RipplerContainer(@NamedArg("container") Node container) {
setContainer(container);
public RipplerContainer(Node container) {
this.container = container;
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
@@ -86,71 +83,79 @@ public class RipplerContainer extends StackPane {
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
containerProperty().addListener(o -> updateChildren());
updateChildren();
InvalidationListener listener = o -> {
if (isSelected()) setBackground(new Background(new BackgroundFill(getRipplerFill(), defaultRadii, null)));
else setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
};
selectedProperty().addListener(listener);
selectedProperty().addListener((a, b, newValue) ->
pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), newValue));
ripplerFillProperty().addListener(listener);
setShape(Lang.apply(new Rectangle(), rectangle -> {
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
}));
var shape = new Rectangle();
shape.widthProperty().bind(widthProperty());
shape.heightProperty().bind(heightProperty());
setShape(shape);
EventHandler<MouseEvent> mouseEventHandler;
if (AnimationUtils.isAnimationEnabled()) {
setOnMouseEntered(e -> new Transition() {
{
setCycleDuration(DURATION);
setInterpolator(Motion.EASE_IN);
mouseEventHandler = event -> {
if (coverAnimation != null) {
coverAnimation.stop();
coverAnimation = null;
}
@Override
protected void interpolate(double frac) {
interpolateBackground(frac);
}
}.play());
if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
coverAnimation = new Transition() {
{
setCycleDuration(Motion.SHORT4);
setInterpolator(Motion.EASE_IN);
}
setOnMouseExited(e -> new Transition() {
{
setCycleDuration(DURATION);
setInterpolator(Motion.EASE_OUT);
@Override
protected void interpolate(double frac) {
interpolateBackground(frac);
}
};
} else {
coverAnimation = new Transition() {
{
setCycleDuration(Motion.SHORT4);
setInterpolator(Motion.EASE_OUT);
}
@Override
protected void interpolate(double frac) {
interpolateBackground(1 - frac);
}
};
}
@Override
protected void interpolate(double frac) {
interpolateBackground(1 - frac);
}
}.play());
coverAnimation.play();
};
} else {
setOnMouseEntered(e -> interpolateBackground(1));
setOnMouseExited(e -> interpolateBackground(0));
mouseEventHandler = event ->
interpolateBackground(event.getEventType() == MouseEvent.MOUSE_ENTERED ? 1 : 0);
}
addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEventHandler);
addEventHandler(MouseEvent.MOUSE_EXITED, mouseEventHandler);
}
private void interpolateBackground(double frac) {
Color onSurface = Themes.getColorScheme().getOnSurface();
setBackground(new Background(new BackgroundFill(
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
CornerRadii.EMPTY, Insets.EMPTY)));
if (frac < 0.01) {
setBackground(null);
} else {
Color onSurface = Themes.getColorScheme().getOnSurface();
setBackground(new Background(new BackgroundFill(
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
CornerRadii.EMPTY, Insets.EMPTY)));
}
}
protected void updateChildren() {
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK)
getChildren().setAll(buttonContainer, getContainer());
else
getChildren().setAll(getContainer(), buttonContainer);
for (int i = 1; i < getChildren().size(); ++i)
getChildren().get(i).setPickOnBounds(false);
Node container = getContainer();
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK) {
getChildren().setAll(buttonContainer, container);
container.setPickOnBounds(false);
} else {
getChildren().setAll(container, buttonContainer);
buttonContainer.setPickOnBounds(false);
}
}
public void setPosition(JFXRippler.RipplerPos pos) {
@@ -158,40 +163,46 @@ public class RipplerContainer extends StackPane {
updateChildren();
}
public Node getContainer() {
return container.get();
public JFXRippler getRippler() {
return buttonRippler;
}
public ObjectProperty<Node> containerProperty() {
public Node getContainer() {
return container;
}
public void setContainer(Node container) {
this.container.set(container);
}
private final StyleableObjectProperty<Paint> ripplerFill = new StyleableObjectProperty<>(DEFAULT_RIPPLER_FILL) {
@Override
public Object getBean() {
return RipplerContainer.this;
}
public Paint getRipplerFill() {
return ripplerFill.get();
}
@Override
public String getName() {
return "ripplerFill";
}
@Override
public CssMetaData<? extends Styleable, Paint> getCssMetaData() {
return StyleableProperties.RIPPLER_FILL;
}
@Override
protected void invalidated() {
buttonRippler.setRipplerFill(get());
}
};
public StyleableObjectProperty<Paint> ripplerFillProperty() {
return ripplerFill;
}
public Paint getRipplerFill() {
return ripplerFillProperty().get();
}
public void setRipplerFill(Paint ripplerFill) {
this.ripplerFill.set(ripplerFill);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
ripplerFillProperty().set(ripplerFill);
}
@Override
@@ -200,12 +211,28 @@ public class RipplerContainer extends StackPane {
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.FACTORY.getCssMetaData();
return StyleableProperties.STYLEABLES;
}
private final static class StyleableProperties {
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<>("-jfx-rippler-fill", PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
@Override
public boolean isSettable(RipplerContainer styleable) {
return styleable.ripplerFill == null || !styleable.ripplerFill.isBound();
}
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
@Override
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer styleable) {
return styleable.ripplerFillProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
styleables.add(RIPPLER_FILL);
STYLEABLES = List.copyOf(styleables);
}
}
}
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
@@ -164,6 +165,8 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof CleanroomInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom")));
} else if (task instanceof LegacyFabricInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.legacyfabric")));
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
@@ -323,8 +326,7 @@ public final class TaskListPane extends StackPane {
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
prevStageNode.status.removeListener(statusChangeListener);
if (item instanceof ProgressListNode) {
var progressListNode = (ProgressListNode) item;
if (item instanceof ProgressListNode progressListNode) {
title.setText(progressListNode.title);
message.textProperty().bind(progressListNode.message);
bar.progressProperty().bind(progressListNode.progress);
@@ -333,8 +335,7 @@ public final class TaskListPane extends StackPane {
pane.setLeft(null);
pane.setRight(message);
pane.setBottom(bar);
} else if (item instanceof StageNode) {
var stageNode = (StageNode) item;
} else if (item instanceof StageNode stageNode) {
title.textProperty().bind(stageNode.title);
message.setText("");
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
@@ -427,23 +428,25 @@ public final class TaskListPane extends StackPane {
// CHECKSTYLE:OFF
// @formatter:off
switch (stageKey) {
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.libraries": message = i18n("libraries.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
case "hmcl.install.cleanroom": message = i18n("install.installer.install", i18n("install.installer.cleanroom") + " " + stageValue); break;
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
message = switch (stageKey) {
case "hmcl.modpack" -> i18n("install.modpack");
case "hmcl.modpack.download" -> i18n("launch.state.modpack");
case "hmcl.install.assets" -> i18n("assets.download");
case "hmcl.install.libraries" -> i18n("libraries.download");
case "hmcl.install.game" -> i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue);
case "hmcl.install.forge" -> i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue);
case "hmcl.install.cleanroom" -> i18n("install.installer.install", i18n("install.installer.cleanroom") + " " + stageValue);
case "hmcl.install.neoforge" -> i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue);
case "hmcl.install.liteloader" -> i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue);
case "hmcl.install.optifine" -> i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue);
case "hmcl.install.fabric" -> i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue);
case "hmcl.install.fabric-api" -> i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue);
case "hmcl.install.legacyfabric" -> i18n("install.installer.install", i18n("install.installer.legacyfabric") + " " + stageValue);
case "hmcl.install.legacyfabric-api" -> i18n("install.installer.install", i18n("install.installer.legacyfabric-api") + " " + stageValue);
case "hmcl.install.quilt" -> i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue);
case "hmcl.install.quilt-api" -> i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue);
default -> i18n(stageKey);
};
// @formatter:on
// CHECKSTYLE:ON
@@ -488,12 +491,10 @@ public final class TaskListPane extends StackPane {
private ProgressListNode(Task<?> task) {
this.title = task.getName();
message.bind(task.messageProperty());
progress.bind(task.progressProperty());
}
public void unbind() {
message.unbind();
progress.unbind();
}
@@ -44,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");
@@ -328,6 +328,18 @@ public class DecoratorSkin extends SkinBase<Decorator> {
}
if (onTitleBarDoubleClick != null)
center.setOnMouseClicked(onTitleBarDoubleClick);
center.setOnMouseDragged(mouseEvent -> {
if (!getSkinnable().isDragging() && primaryStage.isMaximized()) {
getSkinnable().setDragging(true);
mouseInitX = mouseEvent.getScreenX();
mouseInitY = mouseEvent.getScreenY();
primaryStage.setMaximized(false);
stageInitWidth = primaryStage.getWidth();
stageInitHeight = primaryStage.getHeight();
primaryStage.setY(stageInitY = 0);
primaryStage.setX(stageInitX = mouseInitX - stageInitWidth / 2);
}
});
navBar.setCenter(center);
if (canRefresh) {
@@ -24,12 +24,12 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.ui.Controllers;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -38,9 +38,11 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class AbstractInstallersPage extends Control implements WizardPage {
public static final String FABRIC_QUILT_API_TIP = "fabricQuiltApi";
protected final WizardController controller;
protected InstallerItem.InstallerItemGroup group;
@@ -56,8 +58,15 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
String libraryId = library.getLibraryId();
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
library.setOnInstall(() -> {
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
if (!Boolean.TRUE.equals(config().getShownTips().get(FABRIC_QUILT_API_TIP))
&& (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)
|| LibraryAnalyzer.LibraryType.QUILT_API.getPatchId().equals(libraryId)
|| LibraryAnalyzer.LibraryType.LEGACY_FABRIC_API.getPatchId().equals(libraryId))) {
Controllers.dialog(new MessageDialogPane.Builder(
i18n("install.installer.fabric-quilt-api.warning", i18n("install.installer." + libraryId)),
i18n("message.warning"),
MessageDialogPane.MessageType.WARNING
).ok(null).addCancel(i18n("button.do_not_show_again"), () -> config().getShownTips().put(FABRIC_QUILT_API_TIP, true)).build());
}
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
@@ -134,7 +134,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Controllers.prompt(i18n("archive.file.name"), (result, 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));
}
@@ -121,32 +121,20 @@ public class InstallersPage extends AbstractInstallersPage {
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName;
switch (libraryType) {
case FORGE:
loaderName = i18n("install.installer.forge");
break;
case NEO_FORGE:
loaderName = i18n("install.installer.neoforge");
break;
case CLEANROOM:
loaderName = i18n("install.installer.cleanroom");
break;
case FABRIC:
loaderName = i18n("install.installer.fabric");
break;
case LITELOADER:
loaderName = i18n("install.installer.liteloader");
break;
case QUILT:
loaderName = i18n("install.installer.quilt");
break;
case OPTIFINE:
loaderName = i18n("install.installer.optifine");
break;
default:
continue;
}
String loaderName = switch (libraryType) {
case FORGE -> i18n("install.installer.forge");
case NEO_FORGE -> i18n("install.installer.neoforge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric").replace(" ", "_");
case FABRIC -> i18n("install.installer.fabric");
case LITELOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
case OPTIFINE -> i18n("install.installer.optifine");
default -> null;
};
if (loaderName == null)
continue;
nameBuilder.append("-").append(loaderName);
}
@@ -124,37 +124,37 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
}
private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, 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());
}
@@ -38,6 +38,8 @@ import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricAPIRemoteVersion;
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
@@ -270,6 +272,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
@@ -22,10 +22,10 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.Profile;
@@ -41,7 +41,9 @@ import org.jackhuang.hmcl.util.io.Zipper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -132,7 +134,9 @@ public final class ExportWizardProvider implements WizardProvider {
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ".hmcl/hmcl.json");
zip.putFile(tempModpack, "modpack.zip");
zip.putFile(tempModpack, ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH.equals(modpackType)
? "modpack.mrpack"
: "modpack.zip");
Path bg = Metadata.HMCL_CURRENT_DIRECTORY.resolve("background");
if (!Files.isDirectory(bg))
@@ -263,10 +267,10 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() {
dependency = new ModrinthModpackExportTask(
profile.getRepository(),
version,
exportInfo,
modpackFile
profile.getRepository(),
version,
exportInfo,
modpackFile
);
}
@@ -109,7 +109,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
if (controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
if (!packWithLauncher.get() && controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.mrpack"));
fileChooser.setInitialFileName(name.get() + (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ".mrpack"));
} else {
@@ -31,15 +31,15 @@ import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.net.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public class DownloadSettingsPage extends StackPane {
@@ -55,51 +55,37 @@ public class DownloadSettingsPage extends StackPane {
getChildren().setAll(scrollPane);
{
VBox downloadSource = new VBox(8);
var downloadSource = new ComponentList();
downloadSource.getStyleClass().add("card-non-transparent");
{
VBox chooseWrapper = new VBox();
chooseWrapper.setPadding(new Insets(8, 0, 8, 0));
JFXCheckBox chkAutoChooseDownloadSource = new JFXCheckBox(i18n("settings.launcher.download_source.auto"));
chkAutoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
chooseWrapper.getChildren().setAll(chkAutoChooseDownloadSource);
var autoChooseDownloadSource = new LineToggleButton();
autoChooseDownloadSource.setTitle(i18n("settings.launcher.download_source.auto"));
autoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
BorderPane versionListSourcePane = new BorderPane();
versionListSourcePane.setPadding(new Insets(0, 0, 8, 30));
versionListSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty().not());
{
Label label = new Label(i18n("settings.launcher.version_list_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
versionListSourcePane.setLeft(label);
Function<String, String> converter = key -> i18n("download.provider." + key);
Function<String, String> descriptionConverter = key -> {
String bundleKey = "download.provider." + key + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
};
JFXComboBox<String> cboVersionListSource = new JFXComboBox<>();
cboVersionListSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
versionListSourcePane.setRight(cboVersionListSource);
FXUtils.setLimitWidth(cboVersionListSource, 400);
var versionListSourcePane = new LineSelectButton<String>();
versionListSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty().not());
versionListSourcePane.setTitle(i18n("settings.launcher.version_list_source"));
versionListSourcePane.setConverter(converter);
versionListSourcePane.setDescriptionConverter(descriptionConverter);
versionListSourcePane.setItems(DownloadProviders.AUTO_PROVIDERS.keySet());
versionListSourcePane.valueProperty().bindBidirectional(config().versionListSourceProperty());
cboVersionListSource.getItems().setAll(DownloadProviders.AUTO_PROVIDERS.keySet());
selectedItemPropertyFor(cboVersionListSource).bindBidirectional(config().versionListSourceProperty());
}
var downloadSourcePane = new LineSelectButton<String>();
downloadSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty());
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
downloadSourcePane.setConverter(converter);
downloadSourcePane.setDescriptionConverter(descriptionConverter);
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
BorderPane downloadSourcePane = new BorderPane();
downloadSourcePane.setPadding(new Insets(0, 0, 8, 30));
downloadSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty());
{
Label label = new Label(i18n("settings.launcher.download_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
downloadSourcePane.setLeft(label);
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setRight(cboDownloadSource);
FXUtils.setLimitWidth(cboDownloadSource, 420);
cboDownloadSource.getItems().setAll(DownloadProviders.DIRECT_PROVIDERS.keySet());
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
}
downloadSource.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
@@ -38,7 +38,10 @@ import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
@@ -116,7 +119,7 @@ public final class JavaDownloadDialog extends StackPane {
vbox.getChildren().add(prompt);
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion());
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
@@ -248,7 +251,7 @@ public final class JavaDownloadDialog extends StackPane {
for (int i = 0; i < versions.size(); i++) {
DiscoJavaRemoteVersion version = versions.get(i);
if (version.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) {
if (version.getJdkVersion() == GameJavaVersion.LATEST.majorVersion()) {
remoteVersionBox.getSelectionModel().select(i);
return;
}
@@ -21,9 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
@@ -110,17 +108,14 @@ public final class JavaInstallPage extends WizardSinglePage {
{
BorderPane namePane = new BorderPane();
var namePane = new LinePane();
{
Label label = new Label(i18n("java.install.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
namePane.setLeft(label);
namePane.setTitle(i18n("java.install.name"));
nameField = new JFXTextField();
nameField.textProperty().bindBidirectional(control.nameProperty);
FXUtils.setLimitWidth(nameField, 200);
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
namePane.setRight(nameField);
nameField.setValidators(
new RequiredValidator(),
@@ -176,16 +171,9 @@ public final class JavaInstallPage extends WizardSinglePage {
}
private void addInfo(String name, String value) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(name));
Label valueLabel = FXUtils.newSafeTruncatedLabel(value);
FXUtils.copyOnDoubleClick(valueLabel);
BorderPane.setMargin(valueLabel, new Insets(0, 0, 0, 16));
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
pane.setCenter(valueLabel);
LineTextPane pane = new LineTextPane();
pane.setTitle(name);
pane.setText(value);
this.componentList.getContent().add(pane);
}
}
@@ -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
@@ -59,7 +59,6 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
@@ -154,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);
}
@@ -246,7 +247,14 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton = new JFXButton();
menuButton.getStyleClass().add("menu-button");
menuButton.setOnAction(e -> onMenu());
menuButton.setOnAction(e -> GameListPopupMenu.show(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight(),
profile, versions
));
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
@@ -342,19 +350,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL);
}
private void onMenu() {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight()
);
}
private void onUpgrade() {
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
@@ -25,7 +25,6 @@ import javafx.beans.binding.StringBinding;
import javafx.beans.binding.When;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ColorPicker;
@@ -44,7 +43,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;
@@ -79,22 +77,13 @@ public class PersonalizationPage extends StackPane {
ComponentList themeList = new ComponentList();
{
BorderPane brightnessPane = new BorderPane();
var brightnessPane = new LineSelectButton<String>();
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness." + name));
brightnessPane.setItems("auto", "light", "dark");
brightnessPane.valueProperty().bindBidirectional(config().themeBrightnessProperty());
themeList.getContent().add(brightnessPane);
Label left = new Label(i18n("settings.launcher.brightness"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
brightnessPane.setLeft(left);
JFXComboBox<String> cboBrightness = new JFXComboBox<>(FXCollections.observableArrayList(
FXUtils.DARK_MODE != null
? List.of("auto", "light", "dark")
: List.of("light", "dark")
));
cboBrightness.setConverter(FXUtils.stringConverter(name -> i18n("settings.launcher.brightness." + name)));
cboBrightness.valueProperty().bindBidirectional(config().themeBrightnessProperty());
brightnessPane.setRight(cboBrightness);
}
{
@@ -116,13 +105,13 @@ public class PersonalizationPage extends StackPane {
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
}
{
OptionToggleButton titleTransparentButton = new OptionToggleButton();
LineToggleButton titleTransparentButton = new LineToggleButton();
themeList.getContent().add(titleTransparentButton);
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
}
{
OptionToggleButton animationButton = new OptionToggleButton();
LineToggleButton animationButton = new LineToggleButton();
themeList.getContent().add(animationButton);
animationButton.selectedProperty().bindBidirectional(config().animationDisabledProperty());
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
@@ -191,7 +180,7 @@ public class PersonalizationPage extends StackPane {
}
Label textOpacity = new Label();
FXUtils.setLimitWidth(textOpacity, 35);
FXUtils.setLimitWidth(textOpacity, 50);
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
@@ -208,8 +197,7 @@ public class PersonalizationPage extends StackPane {
}
{
ComponentList logPane = new ComponentSublist();
logPane.setTitle(i18n("settings.launcher.log"));
ComponentList logPane = new ComponentList();
{
VBox fontPane = new VBox();
@@ -265,8 +253,7 @@ public class PersonalizationPage extends StackPane {
}
{
ComponentSublist fontPane = new ComponentSublist();
fontPane.setTitle(i18n("settings.launcher.font"));
ComponentList fontPane = new ComponentList();
{
VBox vbox = new VBox();
@@ -308,45 +295,32 @@ public class PersonalizationPage extends StackPane {
}
{
BorderPane fontAntiAliasingPane = new BorderPane();
{
VBox left = new VBox();
Label title = new Label(i18n("settings.launcher.font.anti_aliasing"));
title.getStyleClass().add("title");
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
subtitle.getStyleClass().add("subtitle");
left.getChildren().setAll(title, subtitle);
fontAntiAliasingPane.setLeft(left);
var fontAntiAliasingPane = new LineSelectButton<Optional<FontSmoothingType>>();
fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing"));
fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart"));
fontAntiAliasingPane.setConverter(value ->
value.isPresent()
? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT))
: i18n("settings.launcher.font.anti_aliasing.auto")
);
fontAntiAliasingPane.setItems(
Optional.empty(),
Optional.of(FontSmoothingType.LCD),
Optional.of(FontSmoothingType.GRAY)
);
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.LCD));
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.GRAY));
} else {
fontAntiAliasingPane.setValue(Optional.empty());
}
{
@SuppressWarnings("unchecked")
JFXComboBox<Optional<FontSmoothingType>> cboAntiAliasing = new JFXComboBox<>(FXCollections.observableArrayList(
Optional.empty(),
Optional.of(FontSmoothingType.LCD),
Optional.of(FontSmoothingType.GRAY)
));
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.LCD));
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.GRAY));
} else {
cboAntiAliasing.setValue(Optional.empty());
}
cboAntiAliasing.setConverter(FXUtils.stringConverter(value -> {
if (value.isPresent()) {
return i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT));
} else {
return i18n("settings.launcher.font.anti_aliasing.auto");
}
}));
FXUtils.onChange(cboAntiAliasing.valueProperty(), value ->
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
.orElse(null)));
fontAntiAliasingPane.setRight(cboAntiAliasing);
}
FXUtils.onChange(fontAntiAliasingPane.valueProperty(), value ->
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
.orElse(null)));
fontPane.getContent().add(fontAntiAliasingPane);
}
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -38,17 +35,18 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
import org.jackhuang.hmcl.ui.nbt.NBTFileType;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.Lang;
@@ -67,7 +65,7 @@ import java.util.Locale;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -148,12 +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()));
accountListItem.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
showAccountListPopupMenu(accountListItem);
e.consume();
}
});
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
@@ -174,6 +167,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareVersionPage);
}
FXUtils.onSecondaryButtonClicked(gameListItem, () -> showGameListPopupMenu(gameListItem));
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
@@ -181,6 +175,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
gameItem.setActionButtonVisible(false);
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
// forth item in left sidebar
AdvancedListItem downloadItem = new AdvancedListItem();
@@ -254,34 +249,14 @@ 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,
JFXPopup.PopupHPosition.LEFT,
gameListItem.getWidth(),
0,
getSkinnable().getMainPage().getProfile(),
getSkinnable().getMainPage().getVersions());
}
}
@@ -293,11 +268,21 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
checkedModpack = true;
if (repository.getVersionCount() == 0) {
Path modpackFile = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
if (Files.exists(modpackFile)) {
Path zipModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
Path mrpackModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.mrpack");
Path modpackFile;
if (Files.exists(zipModpack)) {
modpackFile = zipModpack;
} else if (Files.exists(mrpackModpack)) {
modpackFile = mrpackModpack;
} else {
modpackFile = null;
}
if (modpackFile != null) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
.thenApplyAsync(
encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
.thenApplyAsync(modpack -> ModpackHelper
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null)
.executor())
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXRadioButton;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
@@ -40,11 +39,8 @@ import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
@@ -63,15 +59,12 @@ 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;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
@@ -208,7 +201,7 @@ public final class SettingsPage extends ScrollPane {
}
{
OptionToggleButton previewPane = new OptionToggleButton();
LineToggleButton previewPane = new LineToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
@@ -252,37 +245,27 @@ public final class SettingsPage extends ScrollPane {
}
{
BorderPane languagePane = new BorderPane();
VBox left = new VBox();
Label title = new Label(i18n("settings.launcher.language"));
title.getStyleClass().add("title");
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
subtitle.getStyleClass().add("subtitle");
left.getChildren().setAll(title, subtitle);
languagePane.setLeft(left);
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
SupportedLocale currentLocale = I18n.getLocale();
JFXComboBox<SupportedLocale> cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> {
chooseLanguagePane.setConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
}));
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
});
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
FXUtils.setLimitWidth(cboLanguage, 300);
languagePane.setRight(cboLanguage);
settingsPane.getContent().add(languagePane);
settingsPane.getContent().add(chooseLanguagePane);
}
{
OptionToggleButton disableAutoGameOptionsPane = new OptionToggleButton();
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
@@ -330,6 +313,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 +385,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 +408,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 +425,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();
}
@@ -20,13 +20,14 @@ package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.jfoenix.controls.JFXTreeView;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.jackhuang.hmcl.ui.FXUtils;
import java.lang.reflect.Array;
import java.util.EnumMap;
@@ -36,13 +37,22 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class NBTTreeView extends JFXTreeView<Tag> {
public final class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
this.setCellFactory(cellFactory());
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new TreeViewSkin<Tag>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
@@ -28,10 +28,9 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.versions.VersionPage;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public ProfileListItemSkin(ProfileListItem skinnable) {
super(skinnable);
@@ -47,7 +46,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
Node left = VersionPage.wrap(SVG.FOLDER);
Node left = FXUtils.wrap(SVG.FOLDER);
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
@@ -37,7 +37,7 @@ import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
@@ -55,7 +55,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
private final Profile profile;
private final JFXTextField txtProfileName;
private final FileItem gameDir;
private final OptionToggleButton toggleUseRelativePath;
private final LineToggleButton toggleUseRelativePath;
/**
* @param profile null if creating a new profile.
@@ -79,7 +79,6 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
rootPane.setStyle("-fx-padding: 20;");
{
ComponentList componentList = new ComponentList();
componentList.setDepth(1);
{
BorderPane profileNamePane = new BorderPane();
{
@@ -113,7 +112,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
gameDir.setTitle(i18n("profile.instance_directory.choose"));
gameDir.pathProperty().bindBidirectional(location);
toggleUseRelativePath = new OptionToggleButton();
toggleUseRelativePath = new LineToggleButton();
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
@@ -245,19 +245,19 @@ public class TerracottaControllerPage extends StackPane {
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
guest.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(guest, () -> {
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, 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();
}
});
});
@@ -455,7 +455,7 @@ public class TerracottaControllerPage extends StackPane {
exportLogInner.setTitle(i18n("terracotta.export_log"));
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
exportLog.setContent(exportLogInner);
exportLog.getProperties().put("ComponentList.noPadding", true);
ComponentList.setNoPadding(exportLog);
// FIXME: SpinnerPane loses its content width in loading state.
exportLog.minHeightProperty().bind(back.heightProperty());
@@ -559,7 +559,7 @@ public class TerracottaControllerPage extends StackPane {
getChildren().setAll(scrollPane);
}
private ComponentList getThirdPartyDownloadNodes() {
private ComponentSublist getThirdPartyDownloadNodes() {
ComponentSublist locals = new ComponentSublist();
locals.setComponentPadding(false);
@@ -587,7 +587,7 @@ public class TerracottaControllerPage extends StackPane {
i18n("message.info"), MessageDialogPane.MessageType.INFO,
() -> FXUtils.openLink(url)
));
container.getProperties().put("ComponentList.noPadding", true);
ComponentList.setNoPadding(container);
locals.getContent().add(container);
}
return locals;
@@ -666,7 +666,7 @@ public class TerracottaControllerPage extends StackPane {
container.getChildren().setAll(nodes);
}, button.title, button.subTitle, button.left, button.right));
button.getProperties().put("ComponentList.noPadding", true);
ComponentList.setNoPadding(button);
return button;
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.terracotta;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
@@ -24,17 +25,21 @@ import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.main.MainPage;
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
@@ -67,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 -> {
@@ -84,6 +95,13 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
String currentId = mainPage.getCurrentGame();
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> mainPage.getProfile().setSelectedVersion(it.getId()));
FXUtils.onSecondaryButtonClicked(item, () -> GameListPopupMenu.show(item,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.LEFT,
item.getWidth(),
0,
mainPage.getProfile(), mainPage.getVersions()));
})
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
@@ -1,12 +1,10 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
@@ -26,7 +24,6 @@ import java.nio.file.FileSystems;
import java.util.Arrays;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class AdvancedVersionSettingPage extends StackPane implements DecoratorPage {
@@ -44,17 +41,17 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final JFXTextField txtWrapper;
private final JFXTextField txtPreLaunchCommand;
private final JFXTextField txtPostExitCommand;
private final OptionToggleButton noJVMArgsPane;
private final OptionToggleButton noOptimizingJVMArgsPane;
private final OptionToggleButton noGameCheckPane;
private final OptionToggleButton noJVMCheckPane;
private final OptionToggleButton noNativesPatchPane;
private final OptionToggleButton useNativeGLFWPane;
private final OptionToggleButton useNativeOpenALPane;
private final LineToggleButton noJVMArgsPane;
private final LineToggleButton noOptimizingJVMArgsPane;
private final LineToggleButton noGameCheckPane;
private final LineToggleButton noJVMCheckPane;
private final LineToggleButton noNativesPatchPane;
private final LineToggleButton useNativeGLFWPane;
private final LineToggleButton useNativeOpenALPane;
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<Renderer> cboRenderer;
private final LineSelectButton<Renderer> rendererPane;
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
this.profile = profile;
@@ -170,40 +167,31 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
nativesDirItem.getChildren().add(nativesDirHint);
BorderPane rendererPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced.renderer"));
rendererPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
rendererPane.setItems(Renderer.values());
cboRenderer = new JFXComboBox<>();
cboRenderer.getItems().setAll(Renderer.values());
cboRenderer.setConverter(stringConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT))));
rendererPane.setRight(cboRenderer);
BorderPane.setAlignment(cboRenderer, Pos.CENTER_RIGHT);
FXUtils.setLimitWidth(cboRenderer, 300);
}
noJVMArgsPane = new OptionToggleButton();
noJVMArgsPane = new LineToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
noOptimizingJVMArgsPane = new OptionToggleButton();
noOptimizingJVMArgsPane = new LineToggleButton();
noOptimizingJVMArgsPane.setTitle(i18n("settings.advanced.no_optimizing_jvm_args"));
noOptimizingJVMArgsPane.disableProperty().bind(noJVMArgsPane.selectedProperty());
noGameCheckPane = new OptionToggleButton();
noGameCheckPane = new LineToggleButton();
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
noJVMCheckPane = new OptionToggleButton();
noJVMCheckPane = new LineToggleButton();
noJVMCheckPane.setTitle(i18n("settings.advanced.dont_check_jvm_validity"));
noNativesPatchPane = new OptionToggleButton();
noNativesPatchPane = new LineToggleButton();
noNativesPatchPane.setTitle(i18n("settings.advanced.dont_patch_natives"));
useNativeGLFWPane = new OptionToggleButton();
useNativeGLFWPane = new LineToggleButton();
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
useNativeOpenALPane = new OptionToggleButton();
useNativeOpenALPane = new LineToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
workaroundPane.getContent().setAll(
@@ -239,7 +227,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
FXUtils.bindEnum(cboRenderer, versionSetting.rendererProperty());
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
@@ -281,7 +269,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.unbind(txtWrapper, versionSetting.wrapperProperty());
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
FXUtils.unbindEnum(cboRenderer, versionSetting.rendererProperty());
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
@@ -42,20 +43,29 @@ import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
private final Path worldDir;
private final Datapack datapack;
final BooleanProperty readOnly;
public DatapackListPage(WorldManagePage worldManagePage) {
this.worldDir = worldManagePage.getWorld().getFile();
datapack = new Datapack(worldDir.resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
this::installMultiDatapack, this::refresh);
refresh();
}
private void installMultiDatapack(List<Path> datapackPath) {
datapackPath.forEach(this::installSingleDatapack);
if (readOnly.get()) {
Controllers.showToast(i18n("datapack.reload.toast"));
}
}
private void installSingleDatapack(Path datapack) {
try {
this.datapack.installPack(datapack);
@@ -83,7 +93,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null) {
res.forEach(this::installSingleDatapack);
installMultiDatapack(res);
}
datapack.loadFromDir();
@@ -114,16 +114,23 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
);
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
});
JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems()));
JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems()));
removeButton.disableProperty().bind(getSkinnable().readOnly);
enableButton.disableProperty().bind(getSkinnable().readOnly);
disableButton.disableProperty().bind(getSkinnable().readOnly);
selectingToolbar.getChildren().addAll(
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
}),
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
removeButton,
enableButton,
disableButton,
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
@@ -179,7 +186,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new DatapackInfoListCell(listView));
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
this.listView.setItems(filteredList);
@@ -302,7 +309,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView) {
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
super(listView);
HBox container = new HBox(8);
@@ -312,6 +319,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
content.setMouseTransparent(true);
setSelectable();
checkBox.disableProperty().bind(isReadOnlyProperty);
imageView.setFitWidth(32);
imageView.setFitHeight(32);
imageView.setPreserveRatio(true);
@@ -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);
@@ -299,13 +300,13 @@ public class DownloadPage extends Control implements DecoratorPage {
for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.collect(Collectors.toList())) {
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
}
ComponentList sublist = new ComponentList(() -> {
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>(versions.size());
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
@@ -464,13 +465,14 @@ public class DownloadPage extends Control implements DecoratorPage {
box.getChildren().setAll(modItem);
SpinnerPane spinnerPane = new SpinnerPane();
ScrollPane scrollPane = new ScrollPane();
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
ComponentList dependenciesList = new ComponentList();
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
scrollPane.setContent(dependenciesList);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.smoothScrolling(scrollPane);
spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
@@ -70,7 +70,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setActionButtonVisible(false);
addProfileItem.setLeftGraphic(VersionPage.wrap(SVG.ADD_CIRCLE));
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
pane.setFitToWidth(true);
@@ -26,6 +26,7 @@ import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
@@ -34,15 +35,29 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// @author Glavo
public final class GameListPopupMenu extends StackPane {
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
Profile profile, List<Version> versions) {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
}
private final JFXListView<GameItem> listView = new JFXListView<>();
private final BooleanBinding isEmpty = Bindings.isEmpty(listView.getItems());
@@ -76,12 +76,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion, InstallerItem.Style.LIST_ITEM);
// Conventional libraries: game, fabric, forge, cleanroom, neoforge, liteloader, optifine
// Conventional libraries: game, fabric, legacyfabric, forge, cleanroom, neoforge, liteloader, optifine
for (InstallerItem item : group.getLibraries()) {
String libraryId = item.getLibraryId();
// Skip fabric-api and quilt-api
if (libraryId.contains("fabric-api") || libraryId.contains("quilt-api")) {
// Skip fabric-api and quilt-api and legacyfabric-api
if (libraryId.endsWith("-api")) {
continue;
}
@@ -19,34 +19,45 @@ package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.*;
import java.util.stream.Collectors;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
private final String gameVersion;
private final Collection<LocalModFile> mods;
private final Collection<Collection<Task<LocalModFile.ModUpdate>>> dependents;
private final List<Task<LocalModFile.ModUpdate>> dependents;
public ModCheckUpdatesTask(String gameVersion, Collection<LocalModFile> mods) {
this.gameVersion = gameVersion;
this.mods = mods;
dependents = mods.stream().map(mod ->
Task.supplyAsync(Schedulers.io(), () -> {
LocalModFile.ModUpdate candidate = null;
for (RemoteMod.Type type : RemoteMod.Type.values()) {
LocalModFile.ModUpdate update = null;
try {
update = mod.checkUpdates(gameVersion, type.getRemoteModRepository());
} catch (IOException e) {
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
}
if (update == null) {
continue;
}
dependents = mods.stream()
.map(mod ->
Arrays.stream(RemoteMod.Type.values())
.map(type ->
Task.supplyAsync(() -> mod.checkUpdates(gameVersion, type.getRemoteModRepository()))
.setSignificance(TaskSignificance.MAJOR)
.setName(String.format("%s (%s)", mod.getFileName(), type.name())).withCounter("update.checking")
)
.collect(Collectors.toList())
)
.collect(Collectors.toList());
if (candidate == null || candidate.getCandidate().getDatePublished().isBefore(update.getCandidate().getDatePublished())) {
candidate = update;
}
}
return candidate;
}).setName(mod.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
).toList();
setStage("update.checking");
getProperties().put("total", dependents.size() * RemoteMod.Type.values().length);
getProperties().put("total", dependents.size());
}
@Override
@@ -61,7 +72,7 @@ public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
@Override
public Collection<? extends Task<?>> getDependents() {
return dependents.stream().flatMap(Collection::stream).collect(Collectors.toList());
return dependents;
}
@Override
@@ -72,14 +83,7 @@ public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
@Override
public void execute() throws Exception {
setResult(dependents.stream()
.map(tasks -> tasks.stream()
.filter(task -> task.getResult() != null)
.map(Task::getResult)
.filter(modUpdate -> !modUpdate.getCandidates().isEmpty())
.max(Comparator.comparing((LocalModFile.ModUpdate modUpdate) -> modUpdate.getCandidates().get(0).getDatePublished()))
.orElse(null)
)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
.map(Task::getResult)
.filter(Objects::nonNull).toList());
}
}
@@ -152,6 +152,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
supportedLoaders.add(ModLoaderType.FABRIC);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
supportedLoaders.add(ModLoaderType.FORGE);
supportedLoaders.add(ModLoaderType.NEO_FORGED);
@@ -470,29 +470,18 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
FXUtils.runInFX(() -> {
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
String loaderName;
switch (modLoaderType) {
case FORGE:
loaderName = i18n("install.installer.forge");
break;
case CLEANROOM:
loaderName = i18n("install.installer.cleanroom");
break;
case NEO_FORGED:
loaderName = i18n("install.installer.neoforge");
break;
case FABRIC:
loaderName = i18n("install.installer.fabric");
break;
case LITE_LOADER:
loaderName = i18n("install.installer.liteloader");
break;
case QUILT:
loaderName = i18n("install.installer.quilt");
break;
default:
continue;
}
String loaderName = switch (modLoaderType) {
case FORGE -> i18n("install.installer.forge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
case NEO_FORGED -> i18n("install.installer.neoforge");
case FABRIC -> i18n("install.installer.fabric");
case LITE_LOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
default -> null;
};
if (loaderName == null)
continue;
if (title.getTags()
.stream()
.noneMatch(it -> it.getText().equals(loaderName))) {
@@ -664,24 +653,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
warning.add(i18n("mods.warning.loader_mismatch"));
switch (dataItem.getModInfo().getModLoaderType()) {
case FORGE:
content.addTagWarning(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTagWarning(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.addTagWarning(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.addTagWarning(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.addTagWarning(i18n("install.installer.liteloader"));
break;
case QUILT:
content.addTagWarning(i18n("install.installer.quilt"));
break;
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
case NEO_FORGED -> content.addTagWarning(i18n("install.installer.neoforge"));
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
}
}
@@ -134,7 +134,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
modManager,
objects.stream()
.filter(o -> o.enabled.get())
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0)))
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidate()))
.collect(Collectors.toList()));
Controllers.taskDialog(
task.whenComplete(Schedulers.javafx(), exception -> {
@@ -203,7 +203,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
enabled.set(!data.getLocalMod().getModManager().isDisabled(data.getLocalMod().getFile()));
fileName.set(data.getLocalMod().getFileName());
currentVersion.set(data.getCurrentVersion().getVersion());
targetVersion.set(data.getCandidates().get(0).getVersion());
targetVersion.set(data.getCandidate().getVersion());
switch (data.getCurrentVersion().getSelf().getType()) {
case CURSEFORGE:
source.set(i18n("mods.curseforge"));
@@ -9,6 +9,8 @@ import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -140,6 +142,8 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
HBox toolbar = new HBox();
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
@@ -19,8 +19,10 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -30,6 +32,7 @@ import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
@@ -160,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));
}
}));
}
@@ -221,7 +224,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
getItems().addAll(item.children);
}
abstract class Item extends Control implements Comparable<Item> {
abstract sealed class Item implements Comparable<Item> {
boolean isDirectory() {
return this instanceof DirItem;
@@ -258,11 +261,6 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
return this.getName().compareTo(o.getName());
}
@Override
protected Skin<?> createDefaultSkin() {
return new ItemSkin(this);
}
}
private final class BackItem extends Item {
@@ -438,6 +436,10 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
return SVG.SCHEMA;
}
public @Nullable Image getImage() {
return image;
}
Node getIcon(int size) {
if (image == null) {
return super.getIcon(size);
@@ -540,62 +542,122 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
}
}
private static final class ItemSkin extends SkinBase<Item> {
public ItemSkin(Item item) {
super(item);
private static final class Cell extends ListCell<Item> {
BorderPane root = new BorderPane();
private final RipplerContainer graphics;
private final BorderPane root;
private final StackPane left;
private final TwoLineListItem center;
private final HBox right;
private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;
private final Tooltip tooltip = new Tooltip();
public Cell() {
this.root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
{
StackPane left = new StackPane();
left.setMaxSize(32, 32);
left.setPrefSize(32, 32);
left.getChildren().add(item.getIcon(24));
this.left = new StackPane();
left.setPadding(new Insets(0, 8, 0, 0));
Path path = item.getPath();
if (path != null) {
FXUtils.installSlowTooltip(left, path.toAbsolutePath().normalize().toString());
}
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);
this.iconSVG = new SVGPath();
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);
BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
}
{
TwoLineListItem center = new TwoLineListItem();
center.setTitle(item.getName());
center.setSubtitle(item.getDescription());
this.center = new TwoLineListItem();
root.setCenter(center);
}
if (!(item instanceof BackItem)) {
HBox right = new HBox(8);
{
this.right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> item.onReveal());
btnReveal.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem))
item.onReveal();
});
JFXButton btnDelete = new JFXButton();
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
btnDelete.setOnAction(event ->
btnDelete.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem)) {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
item::onDelete, null));
item::onDelete, null);
}
});
right.getChildren().setAll(btnReveal, btnDelete);
root.setRight(right);
}
RipplerContainer container = new RipplerContainer(root);
FXUtils.onClicked(container, item::onClick);
this.getChildren().add(container);
this.graphics = new RipplerContainer(root);
FXUtils.onClicked(graphics, () -> {
Item item = getItem();
if (item != null)
item.onClick();
});
}
@Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
iconImageView.setImage(null);
if (empty || item == null) {
setGraphic(null);
center.setTitle("");
center.setSubtitle("");
} else {
if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) {
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
}
center.setTitle(item.getName());
center.setSubtitle(item.getDescription());
Path path = item.getPath();
if (path != null) {
tooltip.setText(FileUtils.getAbsolutePath(path));
FXUtils.installSlowTooltip(left, tooltip);
} else {
tooltip.setText("");
Tooltip.uninstall(left, tooltip);
}
root.setRight(item instanceof BackItem ? null : right);
setGraphic(graphics);
}
}
}
@@ -612,5 +674,10 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory)
);
}
@Override
protected ListCell<Item> createListCell(JFXListView<Item> listView) {
return new Cell();
}
}
}
@@ -64,6 +64,7 @@ public class VersionIconDialog extends DialogPane {
createIcon(VersionIconType.OPTIFINE),
createIcon(VersionIconType.CRAFT_TABLE),
createIcon(VersionIconType.FABRIC),
createIcon(VersionIconType.LEGACY_FABRIC),
createIcon(VersionIconType.FORGE),
createIcon(VersionIconType.CLEANROOM),
createIcon(VersionIconType.NEO_FORGE),
@@ -21,11 +21,8 @@ import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
@@ -305,20 +302,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(stackPane, 30);
FXUtils.setLimitHeight(stackPane, 20);
stackPane.setPadding(new Insets(0, 0, 0, 0));
stackPane.getChildren().setAll(node);
return stackPane;
}
public static Node wrap(SVG svg) {
return wrap(svg.createIcon(20));
}
public interface VersionLoadable {
void loadVersion(Profile profile, String version);
}
@@ -42,10 +42,8 @@ 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.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
@@ -56,34 +54,17 @@ 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;
@@ -97,7 +78,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final JFXComboBox<String> cboWindowsSize;
private final JFXTextField txtServerIP;
private final ComponentList componentList;
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
private final LineSelectButton<LauncherVisibility> launcherVisibilityPane;
private final JFXCheckBox chkAutoAllocate;
private final JFXCheckBox chkFullscreen;
private final ComponentSublist javaSublist;
@@ -109,9 +90,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final ComponentSublist gameDirSublist;
private final MultiFileItem<GameDirectoryType> gameDirItem;
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
private final JFXComboBox<ProcessPriority> cboProcessPriority;
private final OptionToggleButton showLogsPane;
private final OptionToggleButton enableDebugLogOutputPane;
private final LineSelectButton<ProcessPriority> processPriorityPane;
private final LineToggleButton showLogsPane;
private final LineToggleButton enableDebugLogOutputPane;
private final ImagePickerItem iconPickerItem;
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
@@ -127,11 +108,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
private final BooleanProperty modpack = new SimpleBooleanProperty();
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
public VersionSettingsPage(boolean globalSetting) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
getChildren().setAll(scrollPane);
rootPane = new VBox();
@@ -190,7 +173,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
{
componentList = new ComponentList();
componentList.setDepth(1);
if (!globalSetting) {
BorderPane copyGlobalPane = new BorderPane();
@@ -368,17 +350,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
}
BorderPane launcherVisibilityPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced.launcher_visible"));
launcherVisibilityPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
cboLauncherVisibility = new JFXComboBox<>();
launcherVisibilityPane.setRight(cboLauncherVisibility);
BorderPane.setAlignment(cboLauncherVisibility, Pos.CENTER_RIGHT);
FXUtils.setLimitWidth(cboLauncherVisibility, 300);
}
launcherVisibilityPane = new LineSelectButton<>();
launcherVisibilityPane.setTitle(i18n("settings.advanced.launcher_visible"));
launcherVisibilityPane.setItems(LauncherVisibility.values());
launcherVisibilityPane.setConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
BorderPane dimensionPane = new BorderPane();
{
@@ -408,23 +383,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
}
showLogsPane = new OptionToggleButton();
showLogsPane = new LineToggleButton();
showLogsPane.setTitle(i18n("settings.show_log"));
enableDebugLogOutputPane = new OptionToggleButton();
enableDebugLogOutputPane = new LineToggleButton();
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
BorderPane processPriorityPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced.process_priority"));
processPriorityPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
cboProcessPriority = new JFXComboBox<>();
processPriorityPane.setRight(cboProcessPriority);
BorderPane.setAlignment(cboProcessPriority, Pos.CENTER_RIGHT);
FXUtils.setLimitWidth(cboProcessPriority, 300);
}
processPriorityPane = new LineSelectButton<>();
processPriorityPane.setTitle(i18n("settings.advanced.process_priority"));
processPriorityPane.setConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
processPriorityPane.setDescriptionConverter(e -> {
String bundleKey = "settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
processPriorityPane.setItems(ProcessPriority.values());
GridPane serverPane = new GridPane();
{
@@ -453,23 +424,16 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
}
BorderPane showAdvancedSettingPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced"));
showAdvancedSettingPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
LineNavigationButton showAdvancedSettingPane = new LineNavigationButton();
showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
showAdvancedSettingPane.setOnAction(event -> {
if (lastVersionSetting != null) {
if (advancedVersionSettingPage == null)
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
JFXButton button = FXUtils.newBorderButton(i18n("settings.advanced.modify"));
button.setOnAction(e -> {
if (lastVersionSetting != null) {
if (advancedVersionSettingPage == null)
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
Controllers.navigateForward(advancedVersionSettingPage);
}
});
showAdvancedSettingPane.setRight(button);
}
Controllers.navigateForward(advancedVersionSettingPage);
}
});
componentList.getContent().addAll(
javaSublist,
@@ -504,16 +468,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT))));
cboProcessPriority.getItems().setAll(ProcessPriority.values());
cboProcessPriority.setConverter(stringConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT))));
memoryStatus.set(SystemInfo.getPhysicalMemoryStatus());
componentList.disableProperty().bind(enableSpecificSettings.not());
initMemoryStatusUpdateTask();
}
@Override
@@ -557,8 +512,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
chkFullscreen.selectedProperty().unbindBidirectional(lastVersionSetting.fullscreenProperty());
showLogsPane.selectedProperty().unbindBidirectional(lastVersionSetting.showLogsProperty());
enableDebugLogOutputPane.selectedProperty().unbindBidirectional(lastVersionSetting.enableDebugLogOutputProperty());
FXUtils.unbindEnum(cboLauncherVisibility, lastVersionSetting.launcherVisibilityProperty());
FXUtils.unbindEnum(cboProcessPriority, lastVersionSetting.processPriorityProperty());
launcherVisibilityPane.valueProperty().unbindBidirectional(lastVersionSetting.launcherVisibilityProperty());
processPriorityPane.valueProperty().unbindBidirectional(lastVersionSetting.processPriorityProperty());
lastVersionSetting.usesGlobalProperty().removeListener(usesGlobalListener);
lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener);
@@ -592,8 +547,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
chkFullscreen.selectedProperty().bindBidirectional(versionSetting.fullscreenProperty());
showLogsPane.selectedProperty().bindBidirectional(versionSetting.showLogsProperty());
enableDebugLogOutputPane.selectedProperty().bindBidirectional(versionSetting.enableDebugLogOutputProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
FXUtils.bindEnum(cboProcessPriority, versionSetting.processPriorityProperty());
launcherVisibilityPane.valueProperty().bindBidirectional(versionSetting.launcherVisibilityProperty());
processPriorityPane.valueProperty().bindBidirectional(versionSetting.processPriorityProperty());
if (versionId != null)
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
@@ -809,4 +764,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);
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -61,18 +62,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements WorldManagePage.WorldRefreshable {
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private final World world;
private final Path backupsDir;
private final boolean isReadOnly;
private final BooleanProperty readOnly;
private final Pattern backupFileNamePattern;
public WorldBackupsPage(WorldManagePage worldManagePage) {
this.world = worldManagePage.getWorld();
this.backupsDir = worldManagePage.getBackupsDir();
this.isReadOnly = worldManagePage.isReadOnly();
this.readOnly = worldManagePage.readOnlyProperty();
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
refresh();
@@ -164,7 +165,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
@Override
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
createBackup.setDisable(isReadOnly);
createBackup.disableProperty().bind(getSkinnable().readOnly);
return Arrays.asList(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
@@ -36,7 +36,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -44,17 +43,19 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jetbrains.annotations.PropertyKey;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.Callable;
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -63,8 +64,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldInfoPage extends SpinnerPane {
public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable {
private final WorldManagePage worldManagePage;
private boolean isReadOnly;
private final World world;
private CompoundTag levelDat;
@@ -73,19 +75,7 @@ public final class WorldInfoPage extends SpinnerPane {
public WorldInfoPage(WorldManagePage worldManagePage) {
this.worldManagePage = worldManagePage;
this.world = worldManagePage.getWorld();
this.setLoading(true);
Task.supplyAsync(this::loadWorldInfo)
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
this.levelDat = result;
updateControls();
setLoading(false);
} else {
LOG.warning("Failed to load level.dat", exception);
setFailedReason(i18n("world.info.failed"));
}
})).start();
refresh();
}
private CompoundTag loadWorldInfo() throws IOException {
@@ -97,7 +87,6 @@ public final class WorldInfoPage extends SpinnerPane {
private void updateControls() {
CompoundTag dataTag = levelDat.get("Data");
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -111,22 +100,23 @@ public final class WorldInfoPage extends SpinnerPane {
FXUtils.smoothScrolling(scrollPane);
rootPane.getStyleClass().add("card-list");
ComponentList basicInfo = new ComponentList();
ComponentList worldInfo = new ComponentList();
{
BorderPane worldNamePane = new BorderPane();
var worldNamePane = new LinePane();
{
setLeftLabel(worldNamePane, "world.name");
worldNamePane.setTitle(i18n("world.name"));
JFXTextField worldNameField = new JFXTextField();
setRightTextField(worldNamePane, worldNameField, 200);
Tag tag = dataTag.get("LevelName");
if (tag instanceof StringTag stringTag) {
worldNameField.setText(stringTag.getValue());
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
var worldName = new SimpleStringProperty(worldNameTag.getValue());
FXUtils.bindString(worldNameField, worldName);
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
worldName.addListener((observable, oldValue, newValue) -> {
if (StringUtils.isNotBlank(newValue)) {
try {
world.setWorldName(newValue);
worldManagePage.setTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())));
} catch (Exception e) {
LOG.warning("Failed to set world name", e);
}
@@ -137,25 +127,18 @@ public final class WorldInfoPage extends SpinnerPane {
}
}
BorderPane gameVersionPane = new BorderPane();
var gameVersionPane = new LineTextPane();
{
setLeftLabel(gameVersionPane, "world.info.game_version");
Label gameVersionLabel = new Label();
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
gameVersionPane.setTitle(i18n("world.info.game_version"));
gameVersionPane.setText(world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
}
BorderPane iconPane = new BorderPane();
var iconPane = new LinePane();
{
setLeftLabel(iconPane, "world.icon");
iconPane.setTitle(i18n("world.icon"));
Runnable onClickAction = () -> Controllers.confirm(
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
this::changeWorldIcon,
null
);
FXUtils.limitSize(iconImageView, 32, 32);
{
FXUtils.limitSize(iconImageView, 32, 32);
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
@@ -163,14 +146,20 @@ public final class WorldInfoPage extends SpinnerPane {
JFXButton resetIconButton = new JFXButton();
{
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
editIconButton.setDisable(worldManagePage.isReadOnly());
FXUtils.onClicked(editIconButton, onClickAction);
editIconButton.setDisable(isReadOnly);
editIconButton.setOnAction(event -> Controllers.confirm(
I18n.i18n("world.icon.change.tip"),
I18n.i18n("world.icon.change"),
MessageDialogPane.MessageType.INFO,
this::changeWorldIcon,
null
));
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
editIconButton.getStyleClass().add("toggle-icon4");
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
resetIconButton.setDisable(worldManagePage.isReadOnly());
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
resetIconButton.setDisable(isReadOnly);
resetIconButton.setOnAction(event -> this.clearWorldIcon());
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
resetIconButton.getStyleClass().add("toggle-icon4");
}
@@ -182,17 +171,15 @@ public final class WorldInfoPage extends SpinnerPane {
iconPane.setRight(hBox);
}
BorderPane seedPane = new BorderPane();
var seedPane = new LinePane();
{
setLeftLabel(seedPane, "world.info.random_seed");
seedPane.setTitle(i18n("world.info.random_seed"));
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
StackPane visibilityButton = new StackPane();
{
visibilityButton.setCursor(Cursor.HAND);
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
FXUtils.setLimitWidth(visibilityButton, 12);
FXUtils.setLimitHeight(visibilityButton, 12);
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
}
@@ -214,282 +201,235 @@ public final class WorldInfoPage extends SpinnerPane {
HBox right = new HBox(8);
{
right.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
right.getChildren().setAll(visibilityButton, seedLabel);
seedPane.setRight(right);
}
}
BorderPane lastPlayedPane = new BorderPane();
var worldSpawnPoint = new LineTextPane();
{
setLeftLabel(lastPlayedPane, "world.info.last_played");
Label lastPlayedLabel = new Label();
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
worldSpawnPoint.setTitle(i18n("world.info.spawn"));
String value;
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
value = Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag
? dimensionTag
: new StringTag("SpawnDimension", "minecraft:overworld"))
.formatPosition(posTag);
} else if (dataTag.get("SpawnX") instanceof IntTag intX
&& dataTag.get("SpawnY") instanceof IntTag intY
&& dataTag.get("SpawnZ") instanceof IntTag intZ) {
value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
} else {
value = null;
}
worldSpawnPoint.setText(value);
}
BorderPane timePane = new BorderPane();
var lastPlayedPane = new LineTextPane();
{
setLeftLabel(timePane, "world.info.time");
Label timeLabel = new Label();
setRightTextLabel(timePane, timeLabel, () -> {
Tag tag = dataTag.get("Time");
if (tag instanceof LongTag) {
long days = ((LongTag) tag).getValue() / 24000;
return i18n("world.info.time.format", days);
} else {
return "";
}
});
lastPlayedPane.setTitle(i18n("world.info.last_played"));
lastPlayedPane.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
}
OptionToggleButton allowCheatsButton = new OptionToggleButton();
var timePane = new LineTextPane();
{
timePane.setTitle(i18n("world.info.time"));
if (dataTag.get("Time") instanceof LongTag timeTag) {
Duration duration = Duration.ofSeconds(timeTag.getValue() / 20);
timePane.setText(i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart()));
}
}
var allowCheatsButton = new LineToggleButton();
{
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
Tag tag = dataTag.get("allowCommands");
allowCheatsButton.setDisable(isReadOnly);
checkTagAndSetListener(tag, allowCheatsButton);
bindTagAndToggleButton(dataTag.get("allowCommands"), allowCheatsButton);
}
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
var generateFeaturesButton = new LineToggleButton();
{
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
generateFeaturesButton.setDisable(isReadOnly);
checkTagAndSetListener(tag, generateFeaturesButton);
// generate_features was valid after 20w20a and MapFeatures was before that
if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) {
bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton);
} else {
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
}
}
BorderPane difficultyPane = new BorderPane();
var difficultyButton = new LineSelectButton<Difficulty>();
{
setLeftLabel(difficultyPane, "world.info.difficulty");
difficultyButton.setTitle(i18n("world.info.difficulty"));
difficultyButton.setDisable(worldManagePage.isReadOnly());
difficultyButton.setItems(Difficulty.items);
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
difficultyBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
difficultyPane.setRight(difficultyBox);
Tag tag = dataTag.get("Difficulty");
if (tag instanceof ByteTag byteTag) {
Difficulty difficulty = Difficulty.of(byteTag.getValue());
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
if (difficulty != null) {
difficultyBox.setValue(difficulty);
difficultyBox.valueProperty().addListener((o, oldValue, newValue) -> {
difficultyButton.setValue(difficulty);
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
byteTag.setValue((byte) newValue.ordinal());
difficultyTag.setValue((byte) newValue.ordinal());
saveLevelDat();
}
});
} else {
difficultyBox.setDisable(true);
difficultyButton.setDisable(true);
}
} else {
difficultyBox.setDisable(true);
difficultyButton.setDisable(true);
}
}
OptionToggleButton difficultyLockPane = new OptionToggleButton();
var difficultyLockPane = new LineToggleButton();
{
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
difficultyLockPane.setDisable(isReadOnly);
Tag tag = dataTag.get("DifficultyLocked");
checkTagAndSetListener(tag, difficultyLockPane);
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
}
basicInfo.getContent().setAll(
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane);
worldInfo.getContent().setAll(
worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane,
allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane);
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
}
Tag playerTag = dataTag.get("Player");
if (playerTag instanceof CompoundTag player) {
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
ComponentList playerInfo = new ComponentList();
BorderPane locationPane = new BorderPane();
var locationPane = new LineTextPane();
{
setLeftLabel(locationPane, "world.info.player.location");
Label locationLabel = new Label();
setRightTextLabel(locationPane, locationLabel, () -> {
Dimension dim = Dimension.of(player.get("Dimension"));
if (dim != null) {
String posString = dim.formatPosition(player.get("Pos"));
if (posString != null)
return posString;
}
return "";
});
locationPane.setTitle(i18n("world.info.player.location"));
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
locationPane.setText(dimension.formatPosition(posTag));
}
}
BorderPane lastDeathLocationPane = new BorderPane();
var lastDeathLocationPane = new LineTextPane();
{
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
Label lastDeathLocationLabel = new Label();
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
if (tag instanceof CompoundTag compoundTag) {
Dimension dim = Dimension.of(compoundTag.get("dimension"));
if (dim != null) {
String posString = dim.formatPosition(compoundTag.get("pos"));
if (posString != null)
return posString;
}
lastDeathLocationPane.setTitle(i18n("world.info.player.last_death_location"));
// Valid after 22w14a; prior to this version, the game did not record the last death location data.
if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) {
Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension"));
if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) {
lastDeathLocationPane.setText(dimension.formatPosition(posTag));
}
return "";
});
}
}
BorderPane spawnPane = new BorderPane();
var spawnPane = new LineTextPane();
{
setLeftLabel(spawnPane, "world.info.player.spawn");
Label spawnLabel = new Label();
setRightTextLabel(spawnPane, spawnLabel, () -> {
Dimension dimension;
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
dimension = Dimension.of(respawnTag.get("dimension"));
Tag posTag = respawnTag.get("pos");
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2));
}
} else if (player.get("SpawnX") instanceof IntTag intX
&& player.get("SpawnY") instanceof IntTag intY
&& player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension"));
if (dimension == null) {
return "";
}
return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
}
return "";
});
spawnPane.setTitle(i18n("world.info.player.spawn"));
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
&& intArrayTag.length() >= 3) { // Valid after 25w07a
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
} else if (playerTag.get("SpawnX") instanceof IntTag intX
&& playerTag.get("SpawnY") instanceof IntTag intY
&& playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
spawnPane.setText((playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD)
.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()));
}
}
BorderPane playerGameTypePane = new BorderPane();
var playerGameTypePane = new LineSelectButton<GameType>();
{
setLeftLabel(playerGameTypePane, "world.info.player.game_type");
playerGameTypePane.setTitle(i18n("world.info.player.game_type"));
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
playerGameTypePane.setItems(GameType.items);
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
gameTypeBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
playerGameTypePane.setRight(gameTypeBox);
Tag tag = player.get("playerGameType");
Tag hardcoreTag = dataTag.get("hardcore");
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
if (tag instanceof IntTag intTag) {
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag
&& dataTag.get("hardcore") instanceof ByteTag hardcoreTag) {
boolean isHardcore = hardcoreTag.getValue() == 1;
GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore);
if (gameType != null) {
gameTypeBox.setValue(gameType);
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
playerGameTypePane.setValue(gameType);
playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
if (newValue == GameType.HARDCORE) {
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 1);
}
playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
hardcoreTag.setValue((byte) 1);
} else {
intTag.setValue(newValue.ordinal());
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 0);
}
playerGameTypeTag.setValue(newValue.ordinal());
hardcoreTag.setValue((byte) 0);
}
saveLevelDat();
}
});
} else {
gameTypeBox.setDisable(true);
playerGameTypePane.setDisable(true);
}
} else {
gameTypeBox.setDisable(true);
playerGameTypePane.setDisable(true);
}
}
BorderPane healthPane = new BorderPane();
var healthPane = new LinePane();
{
setLeftLabel(healthPane, "world.info.player.health");
JFXTextField healthField = new JFXTextField();
setRightTextField(healthPane, healthField, 50);
Tag tag = player.get("Health");
if (tag instanceof FloatTag floatTag) {
setTagAndTextField(floatTag, healthField);
} else {
healthField.setDisable(true);
}
healthPane.setTitle(i18n("world.info.player.health"));
setRightTextField(healthPane, 50, playerTag.get("Health"));
}
BorderPane foodLevelPane = new BorderPane();
var foodLevelPane = new LinePane();
{
setLeftLabel(foodLevelPane, "world.info.player.food_level");
JFXTextField foodLevelField = new JFXTextField();
setRightTextField(foodLevelPane, foodLevelField, 50);
Tag tag = player.get("foodLevel");
if (tag instanceof IntTag intTag) {
setTagAndTextField(intTag, foodLevelField);
} else {
foodLevelField.setDisable(true);
}
foodLevelPane.setTitle(i18n("world.info.player.food_level"));
setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel"));
}
BorderPane xpLevelPane = new BorderPane();
var foodSaturationPane = new LinePane();
{
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
JFXTextField xpLevelField = new JFXTextField();
setRightTextField(xpLevelPane, xpLevelField, 50);
foodSaturationPane.setTitle(i18n("world.info.player.food_saturation_level"));
setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel"));
}
Tag tag = player.get("XpLevel");
if (tag instanceof IntTag intTag) {
setTagAndTextField(intTag, xpLevelField);
} else {
xpLevelField.setDisable(true);
}
var xpLevelPane = new LinePane();
{
xpLevelPane.setTitle(i18n("world.info.player.xp_level"));
setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel"));
}
playerInfo.getContent().setAll(
locationPane, lastDeathLocationPane, spawnPane,
playerGameTypePane, healthPane, foodLevelPane, xpLevelPane
locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane,
healthPane, foodLevelPane, foodSaturationPane, xpLevelPane
);
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
}
}
private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = "assets.lang.I18N") String key) {
Label label = new Label(i18n(key));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
borderPane.setLeft(label);
private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) {
JFXTextField textField = new JFXTextField();
setRightTextField(borderPane, textField, perfWidth);
if (tag instanceof IntTag intTag) {
bindTagAndTextField(intTag, textField);
} else if (tag instanceof FloatTag floatTag) {
bindTagAndTextField(floatTag, textField);
} else {
textField.setDisable(true);
}
}
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
textField.setDisable(worldManagePage.isReadOnly());
textField.setDisable(isReadOnly);
textField.setPrefWidth(perfWidth);
textField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(textField, Pos.CENTER_RIGHT);
borderPane.setRight(textField);
}
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
FXUtils.copyOnDoubleClick(label);
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
try {
label.setText(setNameCall.call());
} catch (Exception e) {
LOG.warning("Exception happened when setting name", e);
}
borderPane.setRight(label);
}
private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) {
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
if (tag instanceof ByteTag byteTag) {
byte value = byteTag.getValue();
if (value == 0 || value == 1) {
@@ -511,14 +451,17 @@ public final class WorldInfoPage extends SpinnerPane {
}
}
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
jfxTextField.setText(String.valueOf(intTag.getValue()));
private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
jfxTextField.setText(intTag.getValue().toString());
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
try {
intTag.setValue(Integer.parseInt(newValue));
saveLevelDat();
Integer integer = Lang.toIntOrNull(newValue);
if (integer != null) {
intTag.setValue(integer);
saveLevelDat();
}
} catch (Exception e) {
jfxTextField.setText(oldValue);
LOG.warning("Exception happened when saving level.dat", e);
@@ -529,14 +472,17 @@ public final class WorldInfoPage extends SpinnerPane {
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
}
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue()));
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
try {
floatTag.setValue(Float.parseFloat(newValue));
saveLevelDat();
Float floatValue = Lang.toFloatOrNull(newValue);
if (floatValue != null) {
floatTag.setValue(floatValue);
saveLevelDat();
}
} catch (Exception e) {
jfxTextField.setText(oldValue);
LOG.warning("Exception happened when saving level.dat", e);
@@ -556,6 +502,23 @@ public final class WorldInfoPage extends SpinnerPane {
}
}
@Override
public void refresh() {
this.isReadOnly = worldManagePage.isReadOnly();
this.setLoading(true);
Task.supplyAsync(this::loadWorldInfo)
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
this.levelDat = result;
updateControls();
setLoading(false);
} else {
LOG.warning("Failed to load level.dat", exception);
setFailedReason(i18n("world.info.failed"));
}
})).start();
}
private record Dimension(String name) {
static final Dimension OVERWORLD = new Dimension(null);
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
@@ -565,8 +528,8 @@ public final class WorldInfoPage extends SpinnerPane {
if (tag instanceof IntTag intTag) {
return switch (intTag.getValue()) {
case 0 -> OVERWORLD;
case 1 -> THE_NETHER;
case 2 -> THE_END;
case -1 -> THE_NETHER;
case 1 -> THE_END;
default -> null;
};
} else if (tag instanceof StringTag stringTag) {
@@ -662,12 +625,12 @@ public final class WorldInfoPage extends SpinnerPane {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
fileChooser.setInitialFileName("icon.png");
File file = fileChooser.showOpenDialog(Controllers.getStage());
if (file == null) return;
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
if (iconPath == null) return;
Image image;
try {
image = FXUtils.loadImage(file.toPath());
image = FXUtils.loadImage(iconPath);
} catch (Exception e) {
LOG.warning("Failed to load image", e);
Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR);
@@ -675,16 +638,16 @@ public final class WorldInfoPage extends SpinnerPane {
}
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
Path output = world.getFile().resolve("icon.png");
saveImage(image, output);
saveWorldIcon(iconPath, image, output);
} else {
Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR);
}
}
private void saveImage(Image image, Path path) {
private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) {
Image oldImage = iconImageView.getImage();
try {
PNGJavaFXUtils.writeImage(image, path);
FileUtils.copyFile(sourcePath, targetPath);
iconImageView.setImage(image);
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
} catch (IOException e) {
@@ -53,7 +53,6 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
@@ -65,10 +64,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir;
private Path backupsDir;
private List<World> worlds;
private Profile profile;
private String id;
private String instanceId;
private int refreshCount = 0;
@@ -88,9 +86,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
@Override
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.id = id;
this.instanceId = id;
this.savesDir = profile.getRepository().getSavesDirectory(id);
this.backupsDir = profile.getRepository().getBackupsDirectory(id);
refresh();
}
@@ -100,7 +97,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
} else if (showAll.get()) {
getItems().setAll(worlds);
} else {
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
getItems().setAll(worlds.stream()
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.toList());
@@ -108,7 +105,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
public void refresh() {
if (profile == null || id == null)
if (profile == null || instanceId == null)
return;
int currentRefresh = ++refreshCount;
@@ -116,10 +113,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
// Ensure the game version number is parsed
profile.getRepository().getGameVersion(id);
try (Stream<World> stream = World.getWorlds(savesDir)) {
return stream.toList();
}
profile.getRepository().getGameVersion(instanceId);
return World.getWorlds(savesDir);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (refreshCount != currentRefresh) {
// A newer refresh task is running, discard this result
@@ -156,18 +151,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 -> {
@@ -177,7 +172,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
private void showManagePage(World world) {
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
Controllers.navigate(new WorldManagePage(world, profile, instanceId));
}
public void export(World world) {
@@ -197,11 +192,11 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
public void launch(World world) {
Versions.launchAndEnterWorld(profile, id, world.getFileName());
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
}
public void generateLaunchScript(World world) {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
}
public BooleanProperty showAllProperty() {
@@ -216,14 +211,15 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
@Override
protected List<Node> initializeToolbar(WorldListPage skinnable) {
JFXCheckBox chkShowAll = new JFXCheckBox();
chkShowAll.setText(i18n("world.show_all"));
JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all"));
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
return Arrays.asList(chkShowAll,
return Arrays.asList(
chkShowAll,
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)
);
}
@Override
@@ -240,6 +236,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
private final ImageView imageView;
private final Tooltip leftTooltip;
private final TwoLineListItem content;
private final JFXButton btnLaunch;
public WorldListCell(WorldListPage page) {
this.page = page;
@@ -271,6 +268,17 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
btnLaunch = new JFXButton();
right.getChildren().add(btnLaunch);
btnLaunch.getStyleClass().add("toggle-icon4");
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
btnLaunch.setOnAction(event -> {
World world = getItem();
if (world != null)
page.launch(world);
});
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
@@ -317,8 +325,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
if (world.getGameVersion() != null)
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
if (world.isLocked())
if (world.isLocked()) {
content.addTag(i18n("world.locked"));
btnLaunch.setDisable(true);
} else {
btnLaunch.setDisable(false);
}
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
@@ -329,13 +341,15 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
// Popup Menu
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
boolean worldLocked = world.isLocked();
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
if (world.supportQuickPlay()) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
launchItem.setDisable(world.isLocked());
launchItem.setDisable(worldLocked);
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll(
@@ -354,18 +368,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
if (ChunkBaseApp.supportEndCity(world)) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
}
}
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
exportMenuItem.setDisable(worldLocked);
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
deleteMenuItem.setDisable(worldLocked);
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
boolean worldLocked = world.isLocked();
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
duplicateMenuItem.setDisable(worldLocked);
popupMenu.getContent().addAll(
new MenuSeparator(),
@@ -19,9 +19,7 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
@@ -37,6 +35,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.channels.FileChannel;
@@ -50,143 +49,92 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
private final ObjectProperty<State> state;
private final World world;
private final Path backupsDir;
private final Profile profile;
private final String id;
private final String versionId;
private FileChannel sessionLockChannel;
private boolean loadFailed = false;
private final ObjectProperty<State> state;
private boolean isFirstNavigation = true;
private final BooleanProperty refreshable = new SimpleBooleanProperty(true);
private final BooleanProperty readOnly = new SimpleBooleanProperty(false);
private final TabHeader header;
private final TransitionPane transitionPane = new TransitionPane();
private final TabHeader header = new TabHeader(transitionPane);
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
private final TransitionPane transitionPane = new TransitionPane();
private FileChannel sessionLockChannel;
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
public WorldManagePage(World world, Profile profile, String versionId) {
this.world = world;
this.backupsDir = backupsDir;
this.backupsDir = profile.getRepository().getBackupsDirectory(versionId);
this.profile = profile;
this.id = id;
this.versionId = versionId;
updateSessionLockChannel();
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
try {
world.reloadLevelDat();
this.world.reloadLevelDat();
} catch (IOException e) {
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
loadFailed = true;
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
}
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))));
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
header.select(worldInfoTab);
setCenter(transitionPane);
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
VBox.setVgrow(left, Priority.ALWAYS);
setLeft(left);
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (world.getGameVersion() != null && // old game will not write game version to level.dat
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
header.getTabs().add(datapackTab);
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
left.setTop(sideBar);
AdvancedListBox toolbar = new AdvancedListBox();
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
}
if (ChunkBaseApp.isSupported(world)) {
PopupMenu chunkBasePopupMenu = new PopupMenu();
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
chunkBasePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), chunkBasePopup)
);
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
chunkBasePopupMenu.getContent().add(
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup));
}
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
chunkBaseMenuItem.setOnAction(e ->
chunkBasePopup.show(chunkBaseMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
chunkBaseMenuItem.getWidth(), 0)));
}
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null);
{
PopupMenu managePopupMenu = new PopupMenu();
JFXPopup managePopup = new JFXPopup(managePopupMenu);
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup),
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup),
new MenuSeparator()
);
}
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup),
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup),
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup)
);
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
{
managePopupMenuItem.setOnAction(e ->
managePopup.show(managePopupMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
managePopupMenuItem.getWidth(), 0));
managePopupMenuItem.setDisable(isReadOnly());
});
}
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar);
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
private void onNavigated(Navigator.NavigationEvent event) {
if (loadFailed) {
Platform.runLater(() -> {
fireEvent(new PageCloseEvent());
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
});
@Override
protected @NotNull Skin createDefaultSkin() {
return new Skin(this);
}
@Override
public void refresh() {
updateSessionLockChannel();
try {
world.reloadLevelDat();
} catch (IOException e) {
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
closePageForLoadingFail();
return;
}
for (var tab : header.getTabs()) {
if (tab.getNode() instanceof WorldRefreshable r) {
r.refresh();
}
}
}
private void closePageForLoadingFail() {
Platform.runLater(() -> {
fireEvent(new PageCloseEvent());
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
});
}
private void updateSessionLockChannel() {
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
readOnly.set(sessionLockChannel == null);
}
}
private void onNavigated(Navigator.NavigationEvent event) {
if (isFirstNavigation)
isFirstNavigation = false;
else
refresh();
}
public void onExited(Navigator.NavigationEvent event) {
try {
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
@@ -194,11 +142,24 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
}
}
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, versionId, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
public void setTitle(String title) {
this.state.set(new DecoratorPage.State(title, null, true, true, true));
}
public World getWorld() {
return world;
}
@@ -208,15 +169,123 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
}
public boolean isReadOnly() {
return sessionLockChannel == null;
return readOnly.get();
}
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, id, world.getFileName());
public BooleanProperty readOnlyProperty() {
return readOnly;
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
@Override
public BooleanProperty refreshableProperty() {
return refreshable;
}
public static class Skin extends DecoratorAnimatedPageSkin<WorldManagePage> {
protected Skin(WorldManagePage control) {
super(control);
setCenter(control.transitionPane);
setLeft(getSidebar());
}
private BorderPane getSidebar() {
BorderPane sidebar = new BorderPane();
{
FXUtils.setLimitWidth(sidebar, 200);
VBox.setVgrow(sidebar, Priority.ALWAYS);
}
sidebar.setTop(getTabBar());
sidebar.setBottom(getToolBar());
return sidebar;
}
private AdvancedListBox getTabBar() {
AdvancedListBox tabBar = new AdvancedListBox();
{
getSkinnable().header.getTabs().addAll(getSkinnable().worldInfoTab, getSkinnable().worldBackupsTab);
getSkinnable().header.select(getSkinnable().worldInfoTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (getSkinnable().world.supportDatapacks()) {
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
}
return tabBar;
}
private AdvancedListBox getToolBar() {
AdvancedListBox toolbar = new AdvancedListBox();
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
{
if (getSkinnable().world.supportQuickPlay()) {
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
}
if (ChunkBaseApp.isSupported(getSkinnable().world)) {
PopupMenu chunkBasePopupMenu = new PopupMenu();
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
chunkBasePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(getSkinnable().world), chunkBasePopup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(getSkinnable().world), chunkBasePopup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(getSkinnable().world), chunkBasePopup)
);
if (ChunkBaseApp.supportEndCity(getSkinnable().world)) {
chunkBasePopupMenu.getContent().add(
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(getSkinnable().world), chunkBasePopup));
}
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
chunkBaseMenuItem.setOnAction(e ->
chunkBasePopup.show(chunkBaseMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
chunkBaseMenuItem.getWidth(), 0)));
}
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(getSkinnable().world.getFile()));
{
PopupMenu managePopupMenu = new PopupMenu();
JFXPopup managePopup = new JFXPopup(managePopupMenu);
if (getSkinnable().world.supportQuickPlay()) {
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
new MenuSeparator()
);
}
managePopupMenu.getContent().addAll(
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup),
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup)
);
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
{
managePopupMenuItem.setOnAction(e ->
managePopup.show(managePopupMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
managePopupMenuItem.getWidth(), 0));
managePopupMenuItem.disableProperty().bind(getSkinnable().readOnlyProperty());
});
}
}
return toolbar;
}
}
public interface WorldRefreshable {
void refresh();
}
}
@@ -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);
}
})
@@ -144,7 +144,7 @@ public final class WorldManageUIUtils {
FileChannel lock = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
return lock;
} catch (IOException ignored) {
} catch (WorldLockedException ignored) {
return null;
}
}
@@ -28,6 +28,7 @@ public final class ChunkBaseApp {
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
@@ -52,6 +53,11 @@ public final class ChunkBaseApp {
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
}
public static boolean supportEndCity(@NotNull World world) {
return world.getSeed() != null && world.getGameVersion() != null &&
world.getGameVersion().compareTo(MIN_END_CITY_VERSION) >= 0;
}
public static ChunkBaseApp newBuilder(String app, long seed) {
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
}
@@ -198,7 +198,7 @@ public final class NativePatcher {
public static SupportStatus checkSupportedStatus(GameVersionNumber gameVersion, Platform platform,
OSVersion systemVersion) {
if (platform.equals(Platform.WINDOWS_X86_64)) {
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_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;
+44 -4
View File
@@ -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;
@@ -1849,3 +1857,35 @@
.tooltip .text {
-fx-fill: -monet-inverse-on-surface;
}
/*******************************************************************************
* *
* Line Button *
* *
******************************************************************************/
.line-select-button .svg {
-fx-opacity: 1;
}
.line-select-button .svg:disabled {
-fx-opacity: 0.4;
}
.line-select-button .menu-container .title {
-fx-font-size: 13px;
-fx-text-fill: -monet-on-surface;
}
.line-select-button .menu-container:selected .title {
-fx-text-fill: -monet-primary;
}
.line-select-button .menu-container .subtitle {
-fx-text-fill: -monet-on-surface-variant;
}
.line-select-button .menu-container:selected .subtitle {
-fx-text-fill: -monet-primary;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff. Show more