Compare commits

...
78 Commits
Author SHA1 Message Date
辞庐 ef3c1b8926 fix: MicrosoftAccountLoginPane 缺少 HEADER (#5656) 2026-02-26 21:51:51 +08:00
Wulian233 ade057b819 Fix 5651: 世界管理不应展示 .zip 文件的世界 (#5652) 2026-02-26 21:50:46 +08:00
TNTyep520andGlavo 2b1de479e5 feat (版本列表): 添加 1.21.11-rc3_unobfuscated. (#5655)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-26 21:50:07 +08:00
辞庐 5502a4efad feat: 切换实例隔离设置后自动刷新其他子页面 (#5638) 2026-02-26 21:48:50 +08:00
Wulian233 5c36c00479 修复语言为文言时下载游戏的选择模组加载器页面的版本号未本地化的问题 (#5359) 2026-02-26 21:29:42 +08:00
辞庐 bb92b5ff8a fix: 选择角色弹窗未适配深色模式 (#5605) 2026-02-26 21:19:13 +08:00
3gf8jv4dv 8b90464d30 chore: update copyright year and attribution (#5622) 2026-02-26 21:16:15 +08:00
Calboot aaea473325 [Bugfix] Fix #5421: 修复部分滚动页面滚动时圆角消失的问题 (#5463)
Fixes #5421
2026-02-25 21:31:50 +08:00
3gf8jv4dv e8f9f093f4 chore(l10n): refine tradchinese (#5623) 2026-02-25 21:17:41 +08:00
辞庐andCopilot 9dc493dd02 清理 ToggleButton 4 创建 (#5630)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 21:17:06 +08:00
辞庐 5cf142df0e fix: message.question 未本地化 (#5633) 2026-02-24 21:45:19 +08:00
Glavo 2a6cd17a3c 重新启用 MCIM 加速 (#5030) 2026-02-24 21:21:46 +08:00
Calboot 285e361a5f [Enhancement] 下载界面简介不滚动 (#5458) 2026-02-23 22:00:52 +08:00
辞庐 0b70dc6d22 fix: 本地安装整合包报错 (#5613) 2026-02-23 21:54:56 +08:00
DimeandGlavo c8e2802373 feat: 给大部分直角图标变为圆角图标 (#5554)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-23 21:54:07 +08:00
Glavo 9ac32a308a FetchTask 在重试前添加等待延迟 (#5603) 2026-02-23 21:50:11 +08:00
辞庐 85cc11be1e fix: 账号列表 copy 按钮未正确应用样式 (#5607) 2026-02-23 20:47:53 +08:00
Xirrenandxirren 568fa354ee 清理OAuth Secret相关 (#5614)
Co-authored-by: xirren <xirren@outlook.com>
2026-02-23 20:45:10 +08:00
辞庐 2728be1014 清理 Decorator (#5618) 2026-02-23 20:41:24 +08:00
Glavo 388473850d Add support for Minecraft 26.1-snapshot-8+ to the linux-loongarch64 platform (#5624) 2026-02-23 16:20:35 +08:00
辞庐 1a5b19e7fe fix: JFXSpinner 样式失效 (#5617) 2026-02-23 07:39:24 +08:00
3gf8jv4dv 8d3fa105f7 docs(readme): update contributor count (#5621) 2026-02-23 03:36:48 +08:00
Glavo ed43c07d88 优化设备代码流登录流程 (#5610) 2026-02-22 13:33:53 +08:00
辞庐 586dee4f1b 清理 CrashReporter (#5599) 2026-02-21 23:13:57 +08:00
辞庐andGlavo 917d11009f 迁移微软授权代码流 client_secret 到 PKCE (#5575)
resolves #5566

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-21 22:25:00 +08:00
Glavo 4fa07661bb 优化 MultipleSourceVersionList.refreshAsync 任务名称 (#5602) 2026-02-21 22:19:23 +08:00
辞庐 b1b90e0855 fix: 禁止取消未生效 (#5600) 2026-02-21 22:16:14 +08:00
Glavo 76370099e3 优化 GameVersionNumber.asGameVersion (#5587) 2026-02-21 09:09:27 +08:00
Glavo 2e5e21affa 修复 MultipleSourceVersionList 不会使用备用下载源的问题 (#5585) 2026-02-20 22:19:47 +08:00
Glavo cceef4f8f8 优化无法补全 JavaFX 时的报错信息 (#5586) 2026-02-20 22:16:53 +08:00
mineDiamond 68c291e23a feat: 添加新版本世界指定资源包位置支持 (#5552) 2026-02-20 21:34:10 +08:00
辞庐andGlavo 1e42034a0b fix: 光影下载被错误地显示为“支持中英文搜索” (#5558)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-20 21:23:52 +08:00
辞庐 53e90ea0f4 fix: 日志窗口 Ctrl+C 复制 toast 在主窗口显示 (#5581) 2026-02-20 21:21:31 +08:00
辞庐andGlavo cd989b1058 fix: 通过 cf / mr 直接安装整合包页面返回有一个多余整合包安装选择页面 (#5556)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-20 21:20:50 +08:00
Glavo 5bc5ae8d15 更新 JFXTreeView (#5574) 2026-02-20 21:16:15 +08:00
辞庐 c8183eaa85 fix: 打开微软对话框有一个加载圈一闪而过 (#5584) 2026-02-20 21:12:47 +08:00
Glavo ae522e0027 Add support for Minecraft 26.1-snapshot-8+ to the freebsd-x86_64/linux-arm64/linux-riscv64 platforms (#5583) 2026-02-20 13:56:42 +08:00
Glavo b6fcbb831c 更新 JFXListView (#5569) 2026-02-19 20:45:36 +08:00
Glavo f3fed74fbc 更新 JFXSlider (#5570) 2026-02-19 06:27:34 +08:00
Glavo 010abf037e 更新 JFXProgressBar 和 JFXSpinner (#5565) 2026-02-19 05:08:33 +08:00
Glavo 9202cd6d61 添加 JFXDialogEvent 源码 (#5564) 2026-02-18 21:56:56 +08:00
辞庐 e54fca4acc .gitignore 添加陶瓦日志 (#5561) 2026-02-18 20:39:02 +08:00
Glavo f6658567ea 添加 JFXCheckBox 源码 (#5560) 2026-02-17 23:51:08 +08:00
Glavo ffb1300081 添加 JFXRadioButton 源码 (#5559) 2026-02-17 23:50:50 +08:00
辞庐andFrom: zkitefly 3187c28df0 feat: 实例列表搜索 (#5242)
Co-authored-by: From: zkitefly <z18344203426@qq.com>
2026-02-17 23:27:35 +08:00
辞庐 fb0919f3fb 优化下载页面条目水波纹样式 (#5521) 2026-02-17 23:16:34 +08:00
Damon Lu b418c9d33b 为“渲染器”添加描述 (#5514) 2026-02-17 23:15:32 +08:00
CiiLuandGlavo d2a9024fc8 修复启动游戏补全文件时,任务没有正确归类的问题 (#5430)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-17 23:04:22 +08:00
CiiLuandGlavo 5e5d777dae fix: 安装整合包时任务弹窗可能空白 (#5550)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-17 22:49:55 +08:00
Xirrenandxirren e25e7c4dbd 修复微软登录报错的超链接显示 (#5549)
Co-authored-by: xirren <xirren@outlook.com>
2026-02-17 22:37:22 +08:00
Glavo 313eec4c10 添加 JFXDialogLayout 源代码 (#5546) 2026-02-17 22:36:48 +08:00
Glavo a0fc43ca1e Fix #4960: 解析 Yggdrasil 服务器地址时应当遵循重定向 (#5539) 2026-02-15 22:11:37 +08:00
小宇 b37ec8dd1f Fix #4278: 更新整合包后刷新版本号 (#5536)
fix #4278
2026-02-15 22:06:08 +08:00
CiiLu 167577b9c6 fix: 修复高分屏下新建账户 Tab 文字模糊的问题 (#5532) 2026-02-15 21:22:14 +08:00
Glavo 1c1fd8b1a4 Bump version root to 3.12 (#5545) 2026-02-15 20:31:11 +08:00
3gf8jv4dv 77a86ef963 docs: fix typos [skip ci] (#5542) 2026-02-15 20:28:09 +08:00
Glavo 32a741e2de Fix #5525: 修复 Task 进度异常的问题 (#5543) 2026-02-15 12:30:23 +08:00
CiiLu d1ead74177 fix: 微软登录完成页 svg 图标不会跟随深色模式 (#5541) 2026-02-15 02:55:57 +08:00
NoClassDefFoundErrorandGlavo 80ba1ec1a7 [Feature] 让Java下载页面可以同时下载多个Java (#5394)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-14 22:10:04 +08:00
Glavo 25fef6b5c2 删除 Lang.timer (#5537) 2026-02-14 22:04:04 +08:00
CiiLu 64ce1854f5 feat: 微软登录二维码生成添加 otc 参数 (#5538) 2026-02-14 22:03:23 +08:00
Glavo fb4f0be899 优化 SpinnerPane (#5533) 2026-02-14 20:45:21 +08:00
Glavo eea072cea5 更新微软账户登录对话框 (#5531) 2026-02-14 20:38:07 +08:00
3gf8jv4dv b0cd184e22 fix(l10n): replace mcedit with minecraft wiki's tutorial page (#5524)
resolves #5519
2026-02-13 22:33:48 +08:00
CiiLu 8b3e801a44 增加弹窗图标内容间距 (#5527) 2026-02-13 22:32:46 +08:00
CiiLu 04bb089f3a fix: 修复新建账号界面从微软账号切换到其他方式后下面按钮排版奇怪的问题 (#5523) 2026-02-12 20:41:17 +08:00
CiiLu 4bd0cbc88f fix: 下载游戏版本列表点击左侧图标无法触发水波纹效果 & 光标没有变成手型 (#5520) 2026-02-12 20:39:01 +08:00
CiiLu 9c4c7bd786 fix: 修复安装卡片部分区域点击无法触发水波纹效果的问题 (#5522) 2026-02-12 20:38:23 +08:00
Damon LuandGlavo f79d560605 阻止在 JFXCustomColorPickerDialog 输入非法颜色信息 (#5368)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-11 21:28:30 +08:00
Glavo 90e6782193 创建 SVGContainer 控件 (#5464) 2026-02-11 21:21:03 +08:00
Glavo ca071fd924 Add weak listener registration methods to WeakListenerHolder (#5509) 2026-02-10 22:42:57 +08:00
Glavo 2e26b14322 修复部分请求头中 Accept-Language 字段包含 Java 版本的问题 (#5508) 2026-02-10 22:39:22 +08:00
CiiLu b7fbebae43 feat: 使用授权代码流登录微软帐户 (#5330) 2026-02-10 02:52:52 +08:00
Glavo 33dc39e59e 修复陶瓦联机页面“导出联机日志”按钮填充异常的问题 (#5507) 2026-02-09 23:28:13 +08:00
Glavo a70b8d7baa 使用自定义 EasyTier 服务器节点 (#5504) 2026-02-09 22:54:18 +08:00
NoClassDefFoundErrorandGlavo 64821600c5 Fix #5326 修改在构建文件树时遇到重名文件夹导致读取失败的问题 (#5369)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-02-09 20:42:10 +08:00
dc993b871f feat: 无版本开始游戏不选择支持状态为“不支持”版本 (#5462)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 20:37:02 +08:00
3gf8jv4dv a0bd64ad1f docs: add HMCL_UI_SCALE to contribution guide [skip ci] (#5496) 2026-02-09 20:36:42 +08:00
139 changed files with 5839 additions and 1319 deletions

No files matched your search

-1
View File
@@ -27,7 +27,6 @@ jobs:
run: ./gradlew build --no-daemon --parallel
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
+1
View File
@@ -15,6 +15,7 @@ hs_err_pid*
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
terracotta-log-*
/.java/
/.local/
/.cache/
+2 -2
View File
@@ -28,7 +28,6 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
@@ -60,6 +59,7 @@ dependencies {
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
@@ -129,6 +129,7 @@ val addOpens = listOf(
"javafx.base/javafx.beans.property",
"javafx.graphics/javafx.css",
"javafx.graphics/javafx.stage",
"javafx.graphics/javafx.scene",
"javafx.graphics/com.sun.glass.ui",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.javafx.util",
@@ -152,7 +153,6 @@ val hmclProperties = buildList {
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
@@ -0,0 +1,169 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXCheckBoxSkin;
import javafx.css.*;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXCheckBox is the material design implementation of a checkbox.
/// it shows ripple effect and a custom selection animation.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXCheckBox extends CheckBox {
public JFXCheckBox(String text) {
super(text);
initialize();
}
public JFXCheckBox() {
initialize();
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXCheckBoxSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-check-box'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-check-box";
/// checkbox color property when selected
private StyleableObjectProperty<Paint> checkedColor;
private static final Color DEFAULT_CHECKED_COLOR = Color.valueOf("#0F9D58");
public StyleableObjectProperty<Paint> checkedColorProperty() {
if (checkedColor == null) {
checkedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.CHECKED_COLOR,
JFXCheckBox.this,
"checkedColor",
DEFAULT_CHECKED_COLOR);
}
return this.checkedColor;
}
public Paint getCheckedColor() {
return checkedColor == null ? DEFAULT_CHECKED_COLOR : checkedColor.get();
}
public void setCheckedColor(Paint color) {
this.checkedColor.set(color);
}
/**
* checkbox color property when not selected
*/
private StyleableObjectProperty<Paint> unCheckedColor;
private static final Color DEFAULT_UNCHECKED_COLOR = Color.valueOf("#5A5A5A");
public StyleableObjectProperty<Paint> unCheckedColorProperty() {
if (unCheckedColor == null) {
unCheckedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNCHECKED_COLOR,
JFXCheckBox.this,
"unCheckedColor",
DEFAULT_UNCHECKED_COLOR);
}
return this.unCheckedColor;
}
public Paint getUnCheckedColor() {
return unCheckedColor == null ? DEFAULT_UNCHECKED_COLOR : unCheckedColor.get();
}
public void setUnCheckedColor(Paint color) {
this.unCheckedColor.set(color);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXCheckBox, Paint> CHECKED_COLOR =
new CssMetaData<>("-jfx-checked-color",
PaintConverter.getInstance(), DEFAULT_CHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.checkedColor == null || !control.checkedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.checkedColorProperty();
}
};
private static final CssMetaData<JFXCheckBox, Paint> UNCHECKED_COLOR =
new CssMetaData<>("-jfx-unchecked-color",
PaintConverter.getInstance(), DEFAULT_UNCHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.unCheckedColor == null || !control.unCheckedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.unCheckedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(CheckBox.getClassCssMetaData());
Collections.addAll(styleables,
CHECKED_COLOR,
UNCHECKED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,104 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.controls;
import java.util.List;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class JFXDialogLayout extends StackPane {
private final StackPane heading = new StackPane();
private final StackPane body = new StackPane();
private final FlowPane actions = new FlowPane() {
protected double computeMinWidth(double height) {
if (this.getContentBias() == Orientation.HORIZONTAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minWidth(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getLeft() + this.snapSizeX(maxPref) + insets.getRight();
} else {
return this.computePrefWidth(height);
}
}
protected double computeMinHeight(double width) {
if (this.getContentBias() == Orientation.VERTICAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minHeight(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getTop() + this.snapSizeY(maxPref) + insets.getBottom();
} else {
return this.computePrefHeight(width);
}
}
};
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog-layout";
public JFXDialogLayout() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
VBox layout = new VBox();
this.heading.getStyleClass().add("jfx-layout-heading");
this.heading.getStyleClass().add("title");
this.body.getStyleClass().add("jfx-layout-body");
this.body.prefHeightProperty().bind(this.prefHeightProperty());
this.body.prefWidthProperty().bind(this.prefWidthProperty());
this.actions.getStyleClass().add("jfx-layout-actions");
layout.getChildren().setAll(this.heading, this.body, this.actions);
this.getChildren().add(layout);
}
public ObservableList<Node> getHeading() {
return this.heading.getChildren();
}
public void setHeading(Node... titleContent) {
this.heading.getChildren().setAll(titleContent);
}
public ObservableList<Node> getBody() {
return this.body.getChildren();
}
public void setBody(Node... body) {
this.body.getChildren().setAll(body);
}
public ObservableList<Node> getActions() {
return this.actions.getChildren();
}
public void setActions(Node... actions) {
this.actions.getChildren().setAll(actions);
}
public void setActions(List<? extends Node> actions) {
this.actions.getChildren().setAll(actions);
}
}
@@ -0,0 +1,271 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.Set;
/// material design implementation of ListCell
///
/// By default, JFXListCell will try to create a graphic node for the cell,
/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method.
///
/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to
/// show the ripple effect upon clicking , to change this behavior you can override the
/// method {[#makeChildrenTransparent()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListCell<T> extends ListCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
protected Node cellContent;
private Rectangle clip;
private Timeline gapAnimation;
private boolean playExpandAnimation = false;
private boolean selectionChanged = false;
/**
* {@inheritDoc}
*/
public JFXListCell() {
initialize();
initListeners();
}
/**
* init listeners to update the vertical gap / selection animation
*/
private void initListeners() {
listViewProperty().addListener((listObj, oldList, newList) -> {
if (newList instanceof JFXListView<?> listView) {
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
cellRippler.rippler.setClip(null);
if (newVal.doubleValue() != 0) {
playExpandAnimation = true;
getListView().requestLayout();
} else {
// fake expand state
double gap = clip.getY() * 2;
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
-gap / 2 - (gap * (getIndex())),
Interpolator.EASE_BOTH)
));
gapAnimation.play();
gapAnimation.setOnFinished((finish) -> {
requestLayout();
Platform.runLater(() -> getListView().requestLayout());
});
}
});
selectedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
selectionChanged = true;
}
});
}
});
}
@Override
protected void layoutChildren() {
super.layoutChildren();
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
double gap = getGap();
if (clip == null) {
clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap);
setClip(clip);
} else {
if (gap != 0) {
if (playExpandAnimation || selectionChanged) {
// fake list collapse state
if (playExpandAnimation) {
this.setTranslateY(-gap / 2 + (-gap * (getIndex())));
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
gapAnimation = new Timeline(new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
0,
Interpolator.EASE_BOTH)));
playExpandAnimation = false;
} else if (selectionChanged) {
clip.setY(0);
clip.setHeight(getHeight());
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH),
new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH)
));
}
playExpandAnimation = false;
selectionChanged = false;
gapAnimation.play();
} else {
if (gapAnimation != null) {
gapAnimation.stop();
}
this.setTranslateY(0);
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
}
} else {
this.setTranslateY(0);
clip.setY(0);
clip.setHeight(getHeight());
}
clip.setX(0);
clip.setWidth(getWidth());
}
if (!getChildren().contains(cellRippler)) {
makeChildrenTransparent();
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
}
}
/**
* this method is used to set some nodes in cell content as mouse transparent nodes
* so clicking on them will trigger the ripple effect.
*/
protected void makeChildrenTransparent() {
for (Node child : getChildren()) {
if (child instanceof Label) {
Set<Node> texts = child.lookupAll("Text");
for (Node text : texts) {
text.setMouseTransparent(true);
}
} else if (child instanceof Shape) {
child.setMouseTransparent(true);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
// remove empty (Trailing cells)
setMouseTransparent(true);
setStyle("-fx-background-color:TRANSPARENT;");
} else {
setMouseTransparent(false);
setStyle(null);
if (item instanceof Node newNode) {
setText(null);
Node currentNode = getGraphic();
if (currentNode == null || !currentNode.equals(newNode)) {
cellContent = newNode;
cellRippler.rippler.cacheRippleClip(false);
// build the Cell node
// RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties
if (newNode instanceof JFXRippler newRippler) {
// build cell container from exisiting rippler
cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty());
cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty());
cellRippler.positionProperty().bind(newRippler.positionProperty());
cellContent = newRippler.getControl();
}
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
setGraphic(cellContent);
}
} else {
setText(item == null ? "null" : item.toString());
setGraphic(null);
}
// show cell tooltip if it's toggled in JFXListView
if (getListView() instanceof JFXListView<?> listView && listView.isShowTooltip()) {
if (item instanceof Label label) {
setTooltip(new Tooltip(label.getText()));
} else if (getText() != null) {
setTooltip(new Tooltip(getText()));
}
}
}
}
// Stylesheet Handling *
/**
* Initialize the style class to 'jfx-list-cell'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.setPadding(new Insets(8, 12, 8, 12));
}
@Override
protected double computePrefHeight(double width) {
double gap = getGap();
return super.computePrefHeight(width) + gap;
}
private double getGap() {
return (getListView() instanceof JFXListView<?> listView)
? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0)
: 0;
}
}
@@ -0,0 +1,258 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXListViewSkin;
import javafx.beans.property.*;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import java.util.*;
/// Material design implementation of List View
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListView<T> extends ListView<T> {
/**
* {@inheritDoc}
*/
public JFXListView() {
this.setCellFactory(listView -> new JFXListCell<>());
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXListViewSkin<>(this);
}
private IntegerProperty depth;
public IntegerProperty depthProperty() {
if (depth == null) {
depth = new SimpleIntegerProperty(this, "depth", 0);
}
return depth;
}
public int getDepth() {
return depth != null ? depth.get() : 0;
}
public void setDepth(int depth) {
depthProperty().set(depth);
}
private DoubleProperty currentVerticalGap;
DoubleProperty currentVerticalGapProperty() {
if (currentVerticalGap == null) {
currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap");
}
return currentVerticalGap;
}
private void updateVerticalGap() {
if (isExpanded()) {
currentVerticalGapProperty().set(verticalGap.get());
} else {
currentVerticalGapProperty().set(0);
}
}
/*
* this only works if the items were labels / strings
*/
private BooleanProperty showTooltip;
public final BooleanProperty showTooltipProperty() {
if (showTooltip == null) {
showTooltip = new SimpleBooleanProperty(this, "showTooltip", false);
}
return this.showTooltip;
}
public final boolean isShowTooltip() {
return showTooltip != null && showTooltip.get();
}
public final void setShowTooltip(final boolean showTooltip) {
this.showTooltipProperty().set(showTooltip);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-list-view'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-list-view";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list)
*/
public void propagateMouseEventsToParent() {
this.addEventHandler(MouseEvent.ANY, e -> {
e.consume();
this.getParent().fireEvent(e);
});
}
private StyleableDoubleProperty verticalGap;
public StyleableDoubleProperty verticalGapProperty() {
if (this.verticalGap == null) {
this.verticalGap = new StyleableDoubleProperty(0.0) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "verticalGap";
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.VERTICAL_GAP;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.verticalGap;
}
public Double getVerticalGap() {
return verticalGap == null ? 0.0 : verticalGap.get();
}
public void setVerticalGap(Double gap) {
verticalGapProperty().set(gap);
}
private StyleableBooleanProperty expanded;
public StyleableBooleanProperty expandedProperty() {
if (expanded == null) {
expanded = new StyleableBooleanProperty(false) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "expanded";
}
@Override
public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
return StyleableProperties.EXPANDED;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.expanded;
}
public Boolean isExpanded() {
return expanded != null && expanded.get();
}
public void setExpanded(Boolean expanded) {
expandedProperty().set(expanded);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP =
new CssMetaData<>("-jfx-vertical-gap",
SizeConverter.getInstance(), 0) {
@Override
public boolean isSettable(JFXListView<?> control) {
return control.verticalGap == null || !control.verticalGap.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) {
return control.verticalGapProperty();
}
};
private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED =
new CssMetaData<>("-jfx-expanded",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXListView<?> control) {
// it's only settable if the List is not shown yet
return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound());
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) {
return control.expandedProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ListView.getClassCssMetaData());
Collections.addAll(styleables, VERTICAL_GAP, EXPANDED);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXProgressBarSkin;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Skin;
/// JFXProgressBar is the material design implementation of a progress bar.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXProgressBar extends ProgressBar {
/// Initialize the style class to 'jfx-progress-bar'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar";
public JFXProgressBar() {
initialize();
}
public JFXProgressBar(double progress) {
super(progress);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXProgressBarSkin(this);
}
private void initialize() {
setPrefWidth(200);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
private DoubleProperty secondaryProgress;
public DoubleProperty secondaryProgressProperty() {
if (secondaryProgress == null) {
secondaryProgress = new SimpleDoubleProperty(this, "secondaryProgress", INDETERMINATE_PROGRESS);
}
return secondaryProgress;
}
public double getSecondaryProgress() {
return secondaryProgress == null ? INDETERMINATE_PROGRESS : secondaryProgress.get();
}
public void setSecondaryProgress(double secondaryProgress) {
secondaryProgressProperty().set(secondaryProgress);
}
}
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXRadioButtonSkin;
import javafx.css.*;
import javafx.css.converter.ColorConverter;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXRadioButton is the material design implementation of a radio button.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXRadioButton extends RadioButton {
public JFXRadioButton(String text) {
super(text);
initialize();
}
public JFXRadioButton() {
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXRadioButtonSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/// Initialize the style class to 'jfx-radio-button'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-radio-button";
/// default color used when the radio button is selected
private StyleableObjectProperty<Color> selectedColor;
private static final Color DEFAULT_SELECTED_COLOR = Color.valueOf("#0F9D58");
public final StyleableObjectProperty<Color> selectedColorProperty() {
if (selectedColor == null) {
selectedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.SELECTED_COLOR,
JFXRadioButton.this,
"selectedColor",
DEFAULT_SELECTED_COLOR);
}
return this.selectedColor;
}
public final Color getSelectedColor() {
return selectedColor == null ? DEFAULT_SELECTED_COLOR : this.selectedColorProperty().get();
}
public final void setSelectedColor(final Color selectedColor) {
this.selectedColorProperty().set(selectedColor);
}
/// default color used when the radio button is not selected
private StyleableObjectProperty<Color> unSelectedColor;
private static final Color DEFAULT_UNSELECTED_COLOR = Color.valueOf("#5A5A5A");
public final StyleableObjectProperty<Color> unSelectedColorProperty() {
if (unSelectedColor == null) {
unSelectedColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.UNSELECTED_COLOR,
JFXRadioButton.this,
"unSelectedColor",
DEFAULT_UNSELECTED_COLOR);
}
return this.unSelectedColor;
}
public final Color getUnSelectedColor() {
return unSelectedColor == null ? DEFAULT_UNSELECTED_COLOR : this.unSelectedColorProperty().get();
}
public final void setUnSelectedColor(final Color unSelectedColor) {
this.unSelectedColorProperty().set(unSelectedColor);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXRadioButton, Color> SELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-selected-color",
ColorConverter.getInstance(), DEFAULT_SELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.selectedColor == null || !control.selectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.selectedColorProperty();
}
};
private static final CssMetaData<JFXRadioButton, Color> UNSELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-unselected-color",
ColorConverter.getInstance(), DEFAULT_UNSELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.unSelectedColor == null || !control.unSelectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.unSelectedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(RadioButton.getClassCssMetaData());
Collections.addAll(styleables,
SELECTED_COLOR,
UNSELECTED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,180 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.IndicatorPositionConverter;
import com.jfoenix.skins.JFXSliderSkin;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.scene.control.Skin;
import javafx.scene.control.Slider;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXSlider is the material design implementation of a slider.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSlider extends Slider {
public JFXSlider() {
super(0, 100, 50);
initialize();
}
public JFXSlider(double min, double max, double value) {
super(min, max, value);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSliderSkin(this);
}
private void initialize() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public enum IndicatorPosition {
LEFT, RIGHT
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/// String binding factory for the slider value.
/// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number).
///
///
/// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)):
/// ```java
/// JFXSlider mySlider = ...
/// mySlider.setValueFactory(slider ->
/// Bindings.createStringBinding(
/// () -> ((int) slider.getValue()) + "%",
/// slider.valueProperty()
/// )
/// );
/// ```
///
/// NOTE: might be replaced later with a call back to create the animated thumb node
///
/// @param callback a callback to create the string value binding
private ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactory;
public final ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactoryProperty() {
if (valueFactory == null) {
valueFactory = new SimpleObjectProperty<>(this, "valueFactory");
}
return valueFactory;
}
/// @return the current slider value factory
public final Callback<JFXSlider, StringBinding> getValueFactory() {
return valueFactory == null ? null : valueFactory.get();
}
/// sets custom string binding for the slider text value
///
/// @param valueFactory a callback to create the string value binding
public final void setValueFactory(final Callback<JFXSlider, StringBinding> valueFactory) {
this.valueFactoryProperty().set(valueFactory);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-slider'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-slider";
/// indicates the position of the slider indicator, can be
/// either LEFT or RIGHT
private StyleableObjectProperty<IndicatorPosition> indicatorPosition;
public StyleableObjectProperty<IndicatorPosition> indicatorPositionProperty() {
if (indicatorPosition == null) {
indicatorPosition = new SimpleStyleableObjectProperty<>(
StyleableProperties.INDICATOR_POSITION,
JFXSlider.this,
"indicatorPosition",
IndicatorPosition.LEFT);
}
return this.indicatorPosition;
}
public IndicatorPosition getIndicatorPosition() {
return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get();
}
public void setIndicatorPosition(IndicatorPosition pos) {
indicatorPositionProperty().set(pos);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSlider, IndicatorPosition> INDICATOR_POSITION = new CssMetaData<>(
"-jfx-indicator-position",
IndicatorPositionConverter.getInstance(),
IndicatorPosition.LEFT) {
@Override
public boolean isSettable(JFXSlider control) {
return control.indicatorPosition == null || !control.indicatorPosition.isBound();
}
@Override
public StyleableProperty<IndicatorPosition> getStyleableProperty(JFXSlider control) {
return control.indicatorPositionProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
Slider.getClassCssMetaData());
Collections.addAll(styleables, INDICATOR_POSITION);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,168 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXSpinnerSkin;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Skin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXSpinner is the material design implementation of a loading spinner.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSpinner extends ProgressIndicator {
public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS;
public JFXSpinner() {
this(INDETERMINATE_PROGRESS);
}
public JFXSpinner(double progress) {
super(progress);
init();
}
private void init() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSpinnerSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-spinner'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
private static final double DEFAULT_RADIUS = 12.0;
/**
* specifies the radius of the spinner node, by default it's set to `12.0`
*/
private StyleableDoubleProperty radius;
public final StyleableDoubleProperty radiusProperty() {
if (this.radius == null) {
this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS,
JFXSpinner.this,
"radius",
DEFAULT_RADIUS);
}
return this.radius;
}
public final double getRadius() {
return radius != null ? radius.get() : DEFAULT_RADIUS;
}
public final void setRadius(final double radius) {
this.radiusProperty().set(radius);
}
/// specifies from which angle the spinner should start spinning
private StyleableDoubleProperty startingAngle;
public final StyleableDoubleProperty startingAngleProperty() {
if (this.startingAngle == null) {
startingAngle = new SimpleStyleableDoubleProperty(StyleableProperties.STARTING_ANGLE,
JFXSpinner.this,
"startingAngle",
0.0);
}
return this.startingAngle;
}
public final double getStartingAngle() {
return startingAngle != null ? startingAngle.get() : 0.0;
}
public final void setStartingAngle(final double startingAngle) {
this.startingAngleProperty().set(startingAngle);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSpinner, Number> RADIUS =
new CssMetaData<>("-jfx-radius",
SizeConverter.getInstance(), DEFAULT_RADIUS) {
@Override
public boolean isSettable(JFXSpinner control) {
return control.radius == null || !control.radius.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
return control.radiusProperty();
}
};
private static final CssMetaData<JFXSpinner, Number> STARTING_ANGLE =
new CssMetaData<>("-jfx-starting-angle",
SizeConverter.getInstance(), 0.0) {
@Override
public boolean isSettable(JFXSpinner control) {
return control.startingAngle == null || !control.startingAngle.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
return control.startingAngleProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
Collections.addAll(styleables, RADIUS, STARTING_ANGLE);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import java.lang.ref.WeakReference;
/// JFXTreeCell is simple material design implementation of a tree cell.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeCell<T> extends TreeCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXTreeCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
private HBox hbox;
private final StackPane selectedPane = new StackPane();
private final InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
isEmpty());
private final WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
treeItemGraphicInvalidationListener);
private WeakReference<TreeItem<T>> treeItemRef;
public JFXTreeCell() {
selectedPane.getStyleClass().add("selection-bar");
selectedPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY)));
selectedPane.setPrefWidth(3);
selectedPane.setMouseTransparent(true);
selectedProperty().addListener((o, oldVal, newVal) -> selectedPane.setVisible(newVal ? true : false));
final InvalidationListener treeItemInvalidationListener = observable -> {
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
if (oldTreeItem != null) {
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
}
TreeItem<T> newTreeItem = getTreeItem();
if (newTreeItem != null) {
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
treeItemRef = new WeakReference<>(newTreeItem);
}
};
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
treeItemProperty().addListener(weakTreeItemListener);
if (getTreeItem() != null) {
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
}
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (!getChildren().contains(selectedPane)) {
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
getChildren().add(0, selectedPane);
}
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
cellRippler.releaseRipple();
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
selectedPane.setVisible(isSelected());
}
private void updateDisplay(T item, boolean empty) {
if (item == null || empty) {
hbox = null;
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
if (treeItem != null && treeItem.getGraphic() != null) {
if (item instanceof Node) {
setText(null);
if (hbox == null) {
hbox = new HBox(3);
}
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
setGraphic(hbox);
} else {
hbox = null;
setText(item.toString());
setGraphic(treeItem.getGraphic());
}
} else {
hbox = null;
if (item instanceof Node) {
setText(null);
setGraphic((Node) item);
} else {
setText(item.toString());
setGraphic(null);
}
}
}
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateDisplay(item, empty);
setMouseTransparent(item == null || empty);
}
}
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
/// JFXTreeView is the material design implementation of a TreeView
/// with expand/collapse animation and selection indicator.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeView<T> extends TreeView<T> {
private static final String DEFAULT_STYLE_CLASS = "jfx-tree-view";
public JFXTreeView() {
init();
}
public JFXTreeView(TreeItem<T> root) {
super(root);
init();
}
private void init() {
this.setCellFactory((view) -> new JFXTreeCell<>());
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls.datamodels.treetable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeTableColumn;
/// data model that is used in JFXTreeTableView, it's used to implement
/// the grouping feature.
///
/// **Note:** the data object used in JFXTreeTableView **must** extends this class
///
/// @param <T> is the concrete object of the Tree table
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class RecursiveTreeObject<T> {
/// grouped children objects
private ObservableList<T> children = FXCollections.observableArrayList();
public ObservableList<T> getChildren() {
return children;
}
public void setChildren(ObservableList<T> children) {
this.children = children;
}
/// Whether or not the object is grouped by a specified tree table column
ObjectProperty<TreeTableColumn<T, ?>> groupedColumn = new SimpleObjectProperty<>();
public final ObjectProperty<TreeTableColumn<T, ?>> groupedColumnProperty() {
return this.groupedColumn;
}
public final TreeTableColumn<T, ?> getGroupedColumn() {
return this.groupedColumnProperty().get();
}
public final void setGroupedColumn(final TreeTableColumn<T, ?> groupedColumn) {
this.groupedColumnProperty().set(groupedColumn);
}
/// the value that must be shown when grouped
ObjectProperty<Object> groupedValue = new SimpleObjectProperty<>();
public final ObjectProperty<Object> groupedValueProperty() {
return this.groupedValue;
}
public final Object getGroupedValue() {
return this.groupedValueProperty().get();
}
public final void setGroupedValue(final Object groupedValue) {
this.groupedValueProperty().set(groupedValue);
}
}
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls.events;
import javafx.event.Event;
import javafx.event.EventType;
import java.io.Serial;
/// JFXDialog events, used exclusively by the following methods:
///
/// - [com.jfoenix.controls.JFXDialog#show()]
/// - [com.jfoenix.controls.JFXDialog#close()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXDialogEvent extends Event {
@Serial
private static final long serialVersionUID = 1L;
/// This event occurs when a JFXDialog is closed, no longer visible to the user
/// ( after the exit animation ends )
public static final EventType<JFXDialogEvent> CLOSED = new EventType<>(Event.ANY, "JFX_DIALOG_CLOSED");
/// This event occurs when a JFXDialog is opened, visible to the user
/// ( after the entrance animation ends )
public static final EventType<JFXDialogEvent> OPENED = new EventType<>(Event.ANY, "JFX_DIALOG_OPENED");
/// Construct a new JFXDialog `Event` with the specified event type
///
/// @param eventType the event type
public JFXDialogEvent(EventType<? extends Event> eventType) {
super(eventType);
}
}
@@ -34,14 +34,17 @@ import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* @author Shadi Shaheen
@@ -95,14 +98,17 @@ public class JFXCustomColorPickerDialog extends StackPane {
rgbField.getStyleClass().add("custom-color-field");
rgbField.setPromptText("RGB Color");
rgbField.setTextFormatter(colorCharFormatter());
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hsbField.getStyleClass().add("custom-color-field");
hsbField.setPromptText("HSB Color");
hsbField.setTextFormatter(colorCharFormatter());
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hexField.getStyleClass().add("custom-color-field");
hexField.setPromptText("#HEX Color");
hexField.setTextFormatter(colorCharFormatter());
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
StackPane tabContent = new StackPane();
@@ -404,4 +410,20 @@ public class JFXCustomColorPickerDialog extends StackPane {
dialog.setMinWidth(minWidth);
dialog.setMinHeight(minHeight);
}
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
private static TextFormatter<String> colorCharFormatter() {
return new TextFormatter<>(change -> {
if (!change.isContentChange()) return change;
String ins = StringUtils.toHalfWidth(change.getText());
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
String full = StringUtils.toHalfWidth(change.getControlNewText());
long h = full.chars().filter(c -> c == '#').count();
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
change.setText(ins);
return change;
});
}
}
@@ -32,8 +32,7 @@ public class JFXListViewSkin<T> extends ListViewSkin<T> {
public JFXListViewSkin(final JFXListView<T> listView) {
super(listView);
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue()));
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
FXUtils.smoothScrolling(flow);
@@ -0,0 +1,224 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.utils.JFXNodeUtils;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.skin.ProgressIndicatorSkin;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.util.Duration;
/// # Material Design ProgressBar Skin
///
/// @author Shadi Shaheen
/// @version 2.0
/// @since 2017-10-06
public class JFXProgressBarSkin extends ProgressIndicatorSkin {
private StackPane track;
private StackPane secondaryBar;
private StackPane bar;
private double barWidth = 0;
private double secondaryBarWidth = 0;
private Timeline indeterminateTransition;
private Region clip;
private final TreeShowingProperty treeShowingProperty;
public JFXProgressBarSkin(JFXProgressBar bar) {
super(bar);
this.treeShowingProperty = new TreeShowingProperty(bar);
bar.widthProperty().addListener(observable -> {
updateProgress();
updateSecondaryProgress();
});
registerChangeListener(bar.progressProperty(), (obs) -> updateProgress());
registerChangeListener(bar.secondaryProgressProperty(), obs -> updateSecondaryProgress());
registerChangeListener(bar.visibleProperty(), obs -> updateAnimation());
registerChangeListener(bar.parentProperty(), obs -> updateAnimation());
registerChangeListener(bar.sceneProperty(), obs -> updateAnimation());
unregisterChangeListeners(treeShowingProperty);
unregisterChangeListeners(bar.indeterminateProperty());
registerChangeListener(treeShowingProperty, obs -> this.updateAnimation());
registerChangeListener(bar.indeterminateProperty(), obs -> initialize());
initialize();
getSkinnable().requestLayout();
}
protected void initialize() {
track = new StackPane();
track.getStyleClass().setAll("track");
bar = new StackPane();
bar.getStyleClass().setAll("bar");
secondaryBar = new StackPane();
secondaryBar.getStyleClass().setAll("secondary-bar");
clip = new Region();
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
bar.backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(bar.getBackground(), clip));
getChildren().setAll(track, secondaryBar, bar);
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + bar.prefHeight(width) + bottomInset;
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height);
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
track.resizeRelocate(x, y, w, h);
secondaryBar.resizeRelocate(x, y, secondaryBarWidth, h);
bar.resizeRelocate(x, y, getSkinnable().isIndeterminate() ? w : barWidth, h);
clip.resizeRelocate(0, 0, w, h);
if (getSkinnable().isIndeterminate()) {
createIndeterminateTimeline();
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
indeterminateTransition.play();
}
// apply clip
bar.setClip(clip);
} else if (indeterminateTransition != null) {
clearAnimation();
// remove clip
bar.setClip(null);
}
}
protected void updateSecondaryProgress() {
final JFXProgressBar control = (JFXProgressBar) getSkinnable();
secondaryBarWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
* Math.min(1, Math.max(0, control.getSecondaryProgress()))) / 2.0F;
control.requestLayout();
}
boolean wasIndeterminate = false;
protected void pauseTimeline(boolean pause) {
if (getSkinnable().isIndeterminate()) {
if (indeterminateTransition == null) {
createIndeterminateTimeline();
}
if (pause) {
indeterminateTransition.pause();
} else {
indeterminateTransition.play();
}
}
}
private void updateAnimation() {
final boolean isTreeShowing = treeShowingProperty.get();
if (indeterminateTransition != null) {
pauseTimeline(!isTreeShowing);
} else if (isTreeShowing) {
createIndeterminateTimeline();
}
}
private void updateProgress() {
final ProgressIndicator control = getSkinnable();
final boolean isIndeterminate = control.isIndeterminate();
if (!(isIndeterminate && wasIndeterminate)) {
barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
* Math.min(1, Math.max(0, control.getProgress()))) / 2.0F;
control.requestLayout();
}
wasIndeterminate = isIndeterminate;
}
private void createIndeterminateTimeline() {
if (indeterminateTransition != null) {
clearAnimation();
}
double dur = 1;
ProgressIndicator control = getSkinnable();
final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
indeterminateTransition = new Timeline(new KeyFrame(
Duration.ZERO,
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_IN),
new KeyValue(clip.translateXProperty(), -w / 2, Interpolator.LINEAR)
),
new KeyFrame(
Duration.seconds(0.5 * dur),
new KeyValue(clip.scaleXProperty(), 0.4, Interpolator.LINEAR)
),
new KeyFrame(
Duration.seconds(0.9 * dur),
new KeyValue(clip.translateXProperty(), w / 2, Interpolator.LINEAR)
),
new KeyFrame(
Duration.seconds(1 * dur),
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_OUT)
));
indeterminateTransition.setCycleCount(Timeline.INDEFINITE);
}
private void clearAnimation() {
indeterminateTransition.stop();
indeterminateTransition.getKeyFrames().clear();
indeterminateTransition = null;
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
if (indeterminateTransition != null) {
clearAnimation();
}
}
}
@@ -0,0 +1,291 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXSlider.IndicatorPosition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Slider;
import javafx.scene.control.skin.SliderSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.util.Duration;
/// # Material Design Slider Skin
///
/// rework of JFXSliderSkin by extending Java SliderSkin
/// this solves padding and resizing issues
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSliderSkin extends SliderSkin {
private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min");
private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max");
private final Pane mouseHandlerPane = new Pane();
private final Text sliderValue;
private final StackPane coloredTrack;
private final StackPane thumb;
private final StackPane track;
private final StackPane animatedThumb;
private NumberAxis tickLine;
private Timeline timeline;
private double indicatorRotation;
private double horizontalRotation;
private double shifting;
public JFXSliderSkin(JFXSlider slider) {
super(slider);
track = (StackPane) getSkinnable().lookup(".track");
thumb = (StackPane) getSkinnable().lookup(".thumb");
tickLine = (NumberAxis) getSkinnable().lookup(".axis");
if (tickLine != null) tickLine.setAnimated(false);
coloredTrack = new StackPane();
coloredTrack.getStyleClass().add("colored-track");
coloredTrack.setMouseTransparent(true);
sliderValue = new Text();
sliderValue.getStyleClass().setAll("slider-value");
animatedThumb = new StackPane();
animatedThumb.getStyleClass().add("animated-thumb");
animatedThumb.getChildren().add(sliderValue);
animatedThumb.setMouseTransparent(true);
animatedThumb.setScaleX(0);
animatedThumb.setScaleY(0);
thumb.layoutXProperty().addListener(x -> {
if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL);
});
thumb.layoutYProperty().addListener(y -> {
if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL);
});
addJFXChildren();
getChildren().addListener((ListChangeListener<Node>) c -> {
while (c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(added -> {
if (added instanceof NumberAxis) {
tickLine = (NumberAxis) added;
tickLine.setAnimated(false);
}
});
}
}
});
registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren());
registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren());
registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding());
initListeners();
}
private void addJFXChildren() {
ObservableList<Node> children = getChildren();
Slider slider = getSkinnable();
if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) {
children.add(0, tickLine);
}
if (children.contains(coloredTrack)) return;
children.add(children.indexOf(thumb), coloredTrack);
children.add(children.indexOf(thumb), animatedThumb);
children.add(0, mouseHandlerPane);
}
private void refreshSliderValueBinding() {
sliderValue.textProperty().unbind();
if (((JFXSlider) getSkinnable()).getValueFactory() != null) {
sliderValue.textProperty()
.bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable()));
} else {
sliderValue.textProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().getLabelFormatter() != null) {
return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue());
} else {
return String.valueOf(Math.round(getSkinnable().getValue()));
}
}, getSkinnable().valueProperty()));
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (timeline == null) {
initAnimation(getSkinnable().getOrientation());
}
double prefWidth = animatedThumb.prefWidth(-1);
animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth));
boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
double width, height, layoutX, layoutY;
if (horizontal) {
width = thumb.getLayoutX() - snappedLeftInset();
height = track.getHeight();
layoutX = track.getLayoutX();
layoutY = track.getLayoutY();
animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2);
} else {
height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
width = track.getWidth();
layoutX = track.getLayoutX();
layoutY = thumb.getLayoutY();
animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2);
}
coloredTrack.resizeRelocate(layoutX, layoutY, width, height);
mouseHandlerPane.resizeRelocate(x, y, w, h);
}
private void initializeVariables() {
shifting = 30 + thumb.getWidth();
if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) {
horizontalRotation = -90;
}
if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) {
indicatorRotation = 180;
shifting = -shifting;
}
final double rotationAngle = 45;
sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation);
animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation);
}
private void initListeners() {
// delegate slider mouse events to track node
mouseHandlerPane.setOnMousePressed(this::delegateToTrack);
mouseHandlerPane.setOnMouseReleased(this::delegateToTrack);
mouseHandlerPane.setOnMouseDragged(this::delegateToTrack);
// animate value node
track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
refreshSliderValueBinding();
updateValueStyleClass();
getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass());
}
private void delegateToTrack(MouseEvent event) {
if (!event.isConsumed()) {
event.consume();
track.fireEvent(event);
}
}
private void updateValueStyleClass() {
getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue());
getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue());
}
private void initAnimation(Orientation orientation) {
initializeVariables();
double thumbPos, thumbNewPos;
DoubleProperty layoutProperty;
if (orientation == Orientation.HORIZONTAL) {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutY() - thumb.getHeight();
thumbNewPos = thumbPos - shifting;
} else {
double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1));
thumbPos = thumb.getLayoutY() - height / 2;
thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight();
}
layoutProperty = animatedThumb.translateYProperty();
} else {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutX() - thumb.getWidth();
thumbNewPos = thumbPos - shifting;
} else {
double width = animatedThumb.prefWidth(-1);
thumbPos = thumb.getLayoutX() - width / 2;
thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth();
}
layoutProperty = animatedThumb.translateXProperty();
}
clearAnimation();
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)),
new KeyFrame(
Duration.seconds(0.2),
new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH)));
}
@Override
public void dispose() {
super.dispose();
clearAnimation();
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline.getKeyFrames().clear();
timeline = null;
}
}
}
@@ -0,0 +1,330 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.utils.JFXNodeUtils;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Group;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Arc;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.util.Duration;
/// JFXSpinner material design skin
///
/// @author Shadi Shaheen & Gerard Moubarak
/// @version 1.0
/// @since 2017-09-25
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
private static final Color GREEN_COLOR = Color.valueOf("#0F9D58");
private static final Color RED_COLOR = Color.valueOf("#db4437");
private static final Color YELLOW_COLOR = Color.valueOf("#f4b400");
private static final Color BLUE_COLOR = Color.valueOf("#4285f4");
private JFXSpinner control;
private final TreeShowingProperty treeShowingProperty;
private boolean isValid = false;
private Timeline timeline;
private Arc arc;
private Arc track;
private final StackPane arcPane;
private final Rectangle fillRect;
private double arcLength = -1;
private final Text text;
public JFXSpinnerSkin(JFXSpinner control) {
super(control);
this.control = control;
this.treeShowingProperty = new TreeShowingProperty(control);
arc = new Arc();
arc.setManaged(false);
arc.setStartAngle(0);
arc.setLength(180);
arc.getStyleClass().setAll("arc");
arc.setFill(Color.TRANSPARENT);
arc.setStrokeWidth(3);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
track = new Arc();
track.setManaged(false);
track.setStartAngle(0);
track.setLength(360);
track.setStrokeWidth(3);
track.getStyleClass().setAll("track");
track.setFill(Color.TRANSPARENT);
fillRect = new Rectangle();
fillRect.setFill(Color.TRANSPARENT);
text = new Text();
text.setStyle("-fx-font-size:null");
text.getStyleClass().setAll("text", "percentage");
final Group group = new Group(fillRect, track, arc, text);
group.setManaged(false);
arcPane = new StackPane(group);
arcPane.setPrefSize(50, 50);
getChildren().setAll(arcPane);
// register listeners
registerChangeListener(control.indeterminateProperty(), obs -> initialize());
registerChangeListener(control.progressProperty(), obs -> updateProgress());
registerChangeListener(treeShowingProperty, obs -> updateAnimation());
registerChangeListener(control.sceneProperty(), obs -> updateAnimation());
}
private void initialize() {
if (getSkinnable().isIndeterminate()) {
if (timeline == null) {
createTransition();
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
timeline.play();
}
}
} else {
clearAnimation();
arc.setStartAngle(90);
updateProgress();
}
}
private KeyFrame[] getKeyFrames(double angle, double duration, Paint color) {
KeyFrame[] frames = new KeyFrame[4];
frames[0] = new KeyFrame(Duration.seconds(duration),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 45 + control.getStartingAngle(),
Interpolator.LINEAR));
frames[1] = new KeyFrame(Duration.seconds(duration + 0.4),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 90 + control.getStartingAngle(),
Interpolator.LINEAR));
frames[2] = new KeyFrame(Duration.seconds(duration + 0.7),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 135 + control.getStartingAngle(),
Interpolator.LINEAR));
frames[3] = new KeyFrame(Duration.seconds(duration + 1.1),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 435 + control.getStartingAngle(),
Interpolator.LINEAR),
new KeyValue(arc.strokeProperty(), color, Interpolator.EASE_BOTH));
return frames;
}
private void pauseTimeline(boolean pause) {
if (getSkinnable().isIndeterminate()) {
if (timeline == null) {
createTransition();
}
if (pause) {
timeline.pause();
} else {
timeline.play();
}
}
}
private void updateAnimation() {
final boolean isTreeShowing = treeShowingProperty.get();
if (timeline != null) {
pauseTimeline(!isTreeShowing);
} else if (isTreeShowing) {
createTransition();
}
}
private double computeSize() {
return control.getRadius() * 2 + arc.getStrokeWidth() * 2;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefWidth(-1);
} else {
return computeSize();
}
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefHeight(-1);
} else {
return computeSize();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
final double strokeWidth = arc.getStrokeWidth();
double radius = control.getRadius();
final double arcSize = snapSizeX(radius * 2 + strokeWidth);
arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
updateArcLayout(radius, arcSize);
fillRect.setWidth(arcSize);
fillRect.setHeight(arcSize);
if (!isValid) {
initialize();
isValid = true;
}
if (!getSkinnable().isIndeterminate()) {
arc.setLength(arcLength);
if (text.isVisible()) {
final double progress = control.getProgress();
int intProgress = (int) Math.round(progress * 100.0);
Font font = text.getFont();
text.setFont(Font.font(font.getFamily(), radius / 1.7));
text.setText((progress > 1 ? 100 : intProgress) + "%");
text.relocate((arcSize - text.getLayoutBounds().getWidth()) / 2, (arcSize - text.getLayoutBounds().getHeight()) / 2);
}
}
}
private void updateArcLayout(double radius, double arcSize) {
arc.setRadiusX(radius);
arc.setRadiusY(radius);
arc.setCenterX(arcSize / 2);
arc.setCenterY(arcSize / 2);
track.setRadiusX(radius);
track.setRadiusY(radius);
track.setCenterX(arcSize / 2);
track.setCenterY(arcSize / 2);
track.setStrokeWidth(arc.getStrokeWidth());
}
boolean wasIndeterminate = false;
protected void updateProgress() {
final ProgressIndicator control = getSkinnable();
final boolean isIndeterminate = control.isIndeterminate();
if (!(isIndeterminate && wasIndeterminate)) {
arcLength = -360 * control.getProgress();
control.requestLayout();
}
wasIndeterminate = isIndeterminate;
}
private void createTransition() {
if (!getSkinnable().isIndeterminate()) return;
final Paint initialColor = arc.getStroke();
if (initialColor == null) {
arc.setStroke(BLUE_COLOR);
}
KeyFrame[] blueFrame = getKeyFrames(0, 0, initialColor == null ? BLUE_COLOR : initialColor);
KeyFrame[] redFrame = getKeyFrames(450, 1.4, initialColor == null ? RED_COLOR : initialColor);
KeyFrame[] yellowFrame = getKeyFrames(900, 2.8, initialColor == null ? YELLOW_COLOR : initialColor);
KeyFrame[] greenFrame = getKeyFrames(1350, 4.2, initialColor == null ? GREEN_COLOR : initialColor);
KeyFrame endingFrame = new KeyFrame(Duration.seconds(5.6),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
1845 + control.getStartingAngle(),
Interpolator.LINEAR));
if (timeline != null) {
timeline.stop();
timeline.getKeyFrames().clear();
}
timeline = new Timeline(blueFrame[0],
blueFrame[1],
blueFrame[2],
blueFrame[3],
redFrame[0],
redFrame[1],
redFrame[2],
redFrame[3],
yellowFrame[0],
yellowFrame[1],
yellowFrame[2],
yellowFrame[3],
greenFrame[0],
greenFrame[1],
greenFrame[2],
greenFrame[3],
endingFrame);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setDelay(Duration.ZERO);
timeline.playFromStart();
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline.getKeyFrames().clear();
timeline = null;
}
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
clearAnimation();
arc = null;
track = null;
control = null;
}
}
@@ -19,13 +19,25 @@
package com.jfoenix.utils;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Shadi Shaheen
/// @version 1.0
@@ -58,6 +70,70 @@ public final class JFXNodeUtils {
}
}
private static final @NotNull Function<Node, ObservableBooleanValue> treeVisiblePropertyGetter = initTreeVisiblePropertyGetter();
private static @NotNull Function<Node, ObservableBooleanValue> initTreeVisiblePropertyGetter() {
MethodHandles.Lookup lookup;
try {
lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
} catch (IllegalAccessException e) {
LOG.warning("Failed to get private lookup for Node", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
try {
Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty");
if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) {
LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType());
return JFXNodeUtils::defaultTreeVisibleProperty;
}
MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod)
.asType(MethodType.methodType(ObservableBooleanValue.class, Node.class));
return item -> {
try {
return (ObservableBooleanValue) handle.invokeExact((Node) item);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new AssertionError("Unreachable", e);
}
};
} catch (Exception e) {
LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
}
/// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback
private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) {
return item.visibleProperty();
}
public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) {
return treeVisiblePropertyGetter.apply(item);
}
public static boolean isTreeVisible(Node item) {
return treeVisibleProperty(item).getValue();
}
public static boolean isTreeShowing(Node node) {
if (node == null)
return false;
Scene scene = node.getScene();
if (scene == null)
return false;
Window window = scene.getWindow();
if (window == null || !window.isShowing())
return false;
return isTreeVisible(node);
}
private JFXNodeUtils() {
}
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jfoenix.utils;
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Window;
/**
* Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing
* it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and
* the scene must have a {@link Window} which is currently showing.<p>
*
* This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in
* an observable form.
*/
public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase {
private final ChangeListener<Boolean> windowShowingChangedListener = (obs, old, current) -> updateTreeShowing();
private final ChangeListener<Window> sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current);
private final ChangeListener<Scene> nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current);
private final Node node;
private final ObservableBooleanValue treeVisibleProperty;
private boolean valid;
private boolean treeShowing;
/**
* Constructs a new instance.
*
* @param node a {@link Node} for which the tree showing status should be observed, cannot be null
*/
public TreeShowingProperty(Node node) {
this.node = node;
this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node);
this.node.sceneProperty().addListener(nodeSceneChangedListener);
this.treeVisibleProperty.addListener(windowShowingChangedListener);
sceneChanged(null, node.getScene());
}
@Override
public Object getBean() {
return node;
}
@Override
public String getName() {
return "treeShowing";
}
/**
* Cleans up any listeners that this class may have registered on the {@link Node}
* that was supplied at construction.
*/
public void dispose() {
node.sceneProperty().removeListener(nodeSceneChangedListener);
if (treeVisibleProperty != null)
treeVisibleProperty.removeListener(windowShowingChangedListener);
valid = false; // prevents unregistration from triggering an invalidation notification
sceneChanged(node.getScene(), null);
}
protected void invalidate() {
if (valid) {
valid = false;
fireValueChangedEvent();
}
}
@Override
public boolean get() {
if (!valid) {
updateTreeShowing();
valid = true;
}
return treeShowing;
}
private void sceneChanged(Scene oldScene, Scene newScene) {
if (oldScene != null) {
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
}
if (newScene != null) {
newScene.windowProperty().addListener(sceneWindowChangedListener);
}
windowChanged(
oldScene == null ? null : oldScene.getWindow(),
newScene == null ? null : newScene.getWindow()
);
}
private void windowChanged(Window oldWindow, Window newWindow) {
if (oldWindow != null) {
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
}
if (newWindow != null) {
newWindow.showingProperty().addListener(windowShowingChangedListener);
}
updateTreeShowing();
}
private void updateTreeShowing() {
boolean newValue = JFXNodeUtils.isTreeShowing(node);
if (newValue != treeShowing) {
treeShowing = newValue;
invalidate();
}
}
}
@@ -200,9 +200,6 @@ public final class EntryPoint {
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
@@ -249,11 +249,11 @@ public final class LauncherHelper {
i18n("message.doing"),
() -> launchingLatch.getCount() == 0, 6.95
).withStage("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.java",
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.withStagesHints(
new Task.StagesHint("launch.state.java"),
new Task.StagesHint("launch.state.dependencies", List.of("hmcl.install.assets", "hmcl.install.libraries", "hmcl.modpack.download")),
new Task.StagesHint("launch.state.logging_in"),
new Task.StagesHint("launch.state.waiting_launching"))
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@@ -167,7 +167,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
}
public static boolean isExternalGameNameConflicts(String name) {
@@ -211,23 +211,24 @@ public final class ModpackHelper {
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
default:
throw new UnsupportedModpackException();
}
@@ -241,9 +242,11 @@ public final class ModpackHelper {
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
else
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -22,27 +22,28 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
private final String codeVerifier;
private final String state;
public static String lastlyOpenedURL;
@@ -52,6 +53,34 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
super(port);
this.port = port;
var encoder = Base64.getUrlEncoder().withoutPadding();
var random = new SecureRandom();
{
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
byte[] bytes = new byte[32];
random.nextBytes(bytes);
this.state = encoder.encodeToString(bytes);
}
{
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
byte[] bytes = new byte[64];
random.nextBytes(bytes);
this.codeVerifier = encoder.encodeToString(bytes);
}
}
@Override
public String getCodeVerifier() {
return codeVerifier;
}
@Override
public String getState() {
return state;
}
@Override
@@ -93,19 +122,32 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
String code = query.get("code");
if (code != null) {
if (this.state.equals(query.get("state"))) {
idToken = query.get("id_token");
future.complete(code);
} else if (query.containsKey("state")) {
LOG.warning("Failed to authenticate: invalid state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
} else {
LOG.warning("Failed to authenticate: missing state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
}
} else {
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
LOG.warning("Failed to authenticate: missing authorization code in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -123,7 +165,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
@@ -150,17 +193,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
@Override
public void openBrowser(String url) throws IOException {
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
lastlyOpenedURL = url;
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
switch (grantFlow) {
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
}
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
@@ -168,17 +207,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
@@ -44,13 +45,15 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -310,11 +313,18 @@ public final class TerracottaManager {
public static TerracottaState.HostScanning setScanning() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of(
"player", getPlayerName()
));
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
.thenComposeAsync(nodes -> {
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
query.add(pair("player", getPlayerName()));
for (URI node : nodes) {
query.add(pair("public_nodes", node.toString()));
}
return new GetTask(NetworkUtils.withQuery(
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
)).setSignificance(Task.TaskSignificance.MINOR);
}).start();
new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start();
return new TerracottaState.HostScanning(-1, -1, null);
}
return null;
@@ -323,15 +333,19 @@ public final class TerracottaManager {
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of(
"room", room,
"player", getPlayerName()
));
return new GetTask(uri)
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
.thenComposeAsync(nodes -> {
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
query.add(pair("room", room));
query.add(pair("player", getPlayerName()));
for (URI node : nodes) {
query.add(pair("public_nodes", node.toString()));
}
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
});
} else {
return null;
}
@@ -0,0 +1,103 @@
/*
* 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.terracotta;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class TerracottaNodeList {
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
@JsonSerializable
private record TerracottaNode(String url, @Nullable String region) implements Validation {
@Override
public void validate() throws JsonParseException, TolerableValidationException {
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
try {
new URI(url);
} catch (URISyntaxException e) {
throw new JsonParseException("Invalid URL: " + url, e);
}
}
}
private static volatile List<URI> list;
public static List<URI> fetch() {
List<URI> list = TerracottaNodeList.list;
if (list != null)
return list;
synchronized (TerracottaNodeList.class) {
list = TerracottaNodeList.list;
if (list != null)
return list;
try {
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
if (nodes == null) {
list = List.of();
LOG.info("No available Terracotta nodes found");
} else {
list = nodes.stream()
.filter(node -> {
if (node == null)
return false;
try {
node.validate();
} catch (Exception e) {
LOG.warning("Invalid terracotta node: " + node, e);
return false;
}
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
})
.map(it -> URI.create(it.url()))
.toList();
LOG.info("Terracotta node list: " + list);
}
} catch (Exception e) {
LOG.warning("Failed to fetch terracotta node list", e);
list = List.of();
}
TerracottaNodeList.list = list;
return list;
}
}
private TerracottaNodeList() {
}
}
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -49,10 +49,10 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
}, latch::countDown, false);
Controllers.dialog(pane);
});
latch.await();
@@ -37,7 +37,6 @@ import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
@@ -51,7 +50,6 @@ import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
@@ -417,14 +415,6 @@ public final class FXUtils {
return region.getMaxHeight();
}
public static Node limitingSize(Node node, double width, double height) {
StackPane pane = new StackPane(node);
pane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(pane, width);
FXUtils.setLimitHeight(pane, height);
return pane;
}
public static void limitCellWidth(ListView<?> listView, ListCell<?> cell) {
ReadOnlyDoubleProperty widthProperty;
@@ -1291,6 +1281,13 @@ public final class FXUtils {
return button;
}
public static JFXButton newToggleButton4(SVG icon, int size) {
JFXButton button = new JFXButton();
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(size));
return button;
}
public static Label newSafeTruncatedLabel() {
Label label = new Label();
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
@@ -28,16 +28,15 @@ import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -295,14 +294,14 @@ public class InstallerItem extends Control {
paneWrapper.pseudoClassStateChanged(CARD, control.style == Style.CARD);
if (control.iconType != null) {
ImageView view = new ImageView(control.iconType.getIcon());
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
node.getStyleClass().add("installer-item-image");
pane.getChildren().add(node);
var imageContainer = new ImageContainer(32);
imageContainer.setImage(control.iconType.getIcon());
imageContainer.setMouseTransparent(true);
imageContainer.getStyleClass().add("installer-item-image");
pane.getChildren().add(imageContainer);
if (control.style == Style.CARD) {
VBox.setMargin(node, new Insets(8, 0, 16, 0));
VBox.setMargin(imageContainer, new Insets(8, 0, 16, 0));
}
}
@@ -343,13 +342,12 @@ public class InstallerItem extends Control {
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
HBox buttonsContainer = new HBox();
buttonsContainer.setPickOnBounds(false);
buttonsContainer.setSpacing(8);
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon());
removeButton.getStyleClass().add("toggle-icon4");
JFXButton removeButton = FXUtils.newToggleButton4(SVG.CLOSE);
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
@@ -43,8 +40,12 @@ import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -257,6 +258,7 @@ public final class LogWindow extends Stage {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final Set<ListCell<Log>> selected = new HashSet<>();
private final JFXSnackbar snackbar = new JFXSnackbar();
LogWindowSkin(LogWindowImpl control) {
super(control);
@@ -265,7 +267,7 @@ public final class LogWindow extends Stage {
vbox.setPadding(new Insets(3, 0, 3, 0));
getSkinnable().stackPane.getChildren().setAll(vbox);
getChildren().setAll(getSkinnable().stackPane);
snackbar.registerSnackbarContainer(getSkinnable().stackPane);
{
BorderPane borderPane = new BorderPane();
@@ -381,7 +383,8 @@ public final class LogWindow extends Stage {
}
}
FXUtils.copyText(stringBuilder.toString());
FXUtils.copyText(stringBuilder.toString(), null);
snackbar.fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(i18n("message.copied"))));
}
});
@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.value.ObservableValue;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
@@ -30,6 +28,7 @@ import javafx.scene.shape.SVGPath;
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
/// The view boxes of all icons are normalized to `0 0 24 24`.
public enum SVG {
NONE(""), // Empty Icon
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
@@ -130,6 +129,12 @@ public enum SVG {
public static final double DEFAULT_SIZE = 24;
static void setSize(SVGPath path, double size) {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
}
private final String rawPath;
private String path;
@@ -144,35 +149,20 @@ public enum SVG {
return path;
}
public SVGPath createSVGPath() {
var p = new SVGPath();
p.setContent(getPath());
p.getStyleClass().add("svg");
return p;
public SVGPath createIcon() {
var path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(getPath());
return path;
}
private static Node createIcon(SVGPath path, double size) {
if (size == DEFAULT_SIZE)
return path;
else {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
public SVGContainer createIcon(double size) {
return new SVGContainer(this, size);
}
public Node createIcon() {
return createIcon(DEFAULT_SIZE);
}
public Node createIcon(double size) {
return createIcon(createSVGPath(), size);
}
public Node createIcon(ObservableValue<? extends Paint> color) {
SVGPath p = createSVGPath();
p.fillProperty().bind(color);
return createIcon(p, DEFAULT_SIZE);
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
SVGPath path = createIcon();
path.fillProperty().bind(color);
return path;
}
}
@@ -0,0 +1,175 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Parent;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
/// A lightweight wrapper for displaying [SVG] icons.
///
/// @author Glavo
public final class SVGContainer extends Parent {
private static final String DEFAULT_STYLE_CLASS = "svg-container";
private final SVGPath path = new SVGPath();
private SVG icon = SVG.NONE;
private double iconSize = SVG.DEFAULT_SIZE;
private SVGPath tempPath;
private Timeline timeline;
{
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.path.getStyleClass().add("svg");
}
/// Creates an SVGContainer with the default icon and the default icon size.
public SVGContainer() {
this(SVG.NONE, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer showing the given icon using the default icon size.
///
/// @param icon the [SVG] icon to display
public SVGContainer(SVG icon) {
this(icon, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer with a custom icon size. The initial icon is
/// [SVG#NONE].
///
/// @param iconSize the icon size
public SVGContainer(double iconSize) {
this(SVG.NONE, iconSize);
}
/// Creates an SVGContainer with the specified icon and size.
///
/// @param icon the [SVG] icon to display
/// @param iconSize the icon size
public SVGContainer(SVG icon, double iconSize) {
setIconSizeImpl(iconSize);
setIcon(icon);
}
public double getIconSize() {
return iconSize;
}
private void setIconSizeImpl(double newSize) {
this.iconSize = newSize;
SVG.setSize(path, newSize);
if (tempPath != null)
SVG.setSize(tempPath, newSize);
}
public void setIconSize(double newSize) {
setIconSizeImpl(newSize);
requestLayout();
}
/// Gets the currently displayed icon.
public SVG getIcon() {
return icon;
}
/// Sets the icon to display without animation.
public void setIcon(SVG newIcon) {
setIcon(newIcon, Duration.ZERO);
}
/// Sets the icon to display with a cross-fade animation.
public void setIcon(SVG newIcon, Duration animationDuration) {
if (timeline != null) {
timeline.stop();
timeline = null;
}
SVG oldIcon = this.icon;
this.icon = newIcon;
if (animationDuration.equals(Duration.ZERO)) {
path.setContent(newIcon.getPath());
path.setOpacity(1);
if (getChildren().size() != 1)
getChildren().setAll(path);
} else {
if (tempPath == null) {
tempPath = new SVGPath();
tempPath.getStyleClass().add("svg");
SVG.setSize(tempPath, iconSize);
} else
tempPath.setOpacity(1);
tempPath.setContent(oldIcon.getPath());
getChildren().setAll(path, tempPath);
path.setOpacity(0);
path.setContent(newIcon.getPath());
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
),
new KeyFrame(animationDuration,
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
)
);
timeline.setOnFinished(e -> {
getChildren().setAll(path);
timeline = null;
});
timeline.play();
}
}
// Parent
@Override
public double prefWidth(double height) {
return iconSize;
}
@Override
public double prefHeight(double width) {
return iconSize;
}
@Override
public double minHeight(double width) {
return iconSize;
}
@Override
public double minWidth(double height) {
return iconSize;
}
@Override
protected void layoutChildren() {
// Do nothing
}
}
@@ -20,14 +20,19 @@ package org.jackhuang.hmcl.ui;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.EventPriority;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class WeakListenerHolder {
public final class WeakListenerHolder {
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
@@ -48,6 +53,22 @@ public class WeakListenerHolder {
return new WeakListChangeListener<>(listener);
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer) {
refs.add(manager.registerWeak(consumer));
}
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer, EventPriority priority) {
refs.add(manager.registerWeak(consumer, priority));
}
public <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChange(value, consumer));
}
public <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
refs.add(FXUtils.onWeakChangeAndOperate(value, consumer));
}
public void add(Object obj) {
refs.add(obj);
}
@@ -125,7 +125,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
JFXButton btnRefresh = new JFXButton();
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
@@ -144,13 +144,11 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
})
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh"));
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
JFXButton btnUpload = new JFXButton();
JFXButton btnUpload = FXUtils.newToggleButton4(SVG.CHECKROOM);
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnAction(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
@@ -161,29 +159,23 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
.start();
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.CHECKROOM.createIcon());
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
btnUpload.disableProperty().bind(Bindings.not(skinnable.canUploadSkin()));
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
right.getChildren().add(spinnerUpload);
JFXButton btnCopyUUID = new JFXButton();
JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnUpload.getStyleClass().add("toggle-icon4");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.CONTENT_COPY.createIcon());
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
spinnerCopyUUID.setContent(btnCopyUUID);
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = new JFXButton();
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE);
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.DELETE.createIcon());
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -130,7 +130,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftIcon(SVG.MICROSOFT);
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
@@ -23,9 +23,7 @@ import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.StringUtils;
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final JFXButton btnAccept;
private final SpinnerPane spinner;
private final Node body;
private final HBox actions;
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnCancel.setOnAction(e -> onCancel());
onEscPressed(this, btnCancel::fire);
HBox hbox = new HBox(spinner, btnCancel);
hbox.setAlignment(Pos.CENTER_RIGHT);
actions = new HBox(spinner, btnCancel);
actions.setAlignment(Pos.CENTER_RIGHT);
setActions(lblErrorMessage, hbox);
setActions(lblErrorMessage, actions);
}
if (showMethodSwitcher) {
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
Runnable doCreate = () -> {
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@@ -280,93 +273,16 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnAccept.disableProperty().unbind();
detailsContainer.getChildren().remove(detailsPane);
lblErrorMessage.setText("");
lblErrorMessage.setVisible(true);
setActions(lblErrorMessage, actions);
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
detailsPane = vbox;
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().add(hintPane);
return;
}
if (!IntegrityChecker.isOfficial()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("unofficial.hint"));
vbox.getChildren().add(hintPane);
}
VBox codeBox = new VBox(8);
Label hint = new Label(i18n("account.methods.microsoft.code"));
Label code = new Label();
code.setMouseTransparent(true);
code.setStyle("-fx-font-size: 24");
codeBox.getChildren().addAll(hint, code);
codeBox.setAlignment(Pos.CENTER);
vbox.getChildren().add(codeBox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setVisible(false);
errHintPane.setManaged(false);
codeBox.setVisible(false);
codeBox.setManaged(false);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
code.setText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
codeBox.setVisible(true);
codeBox.setManaged(true);
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
codeBox.setVisible(false);
codeBox.setManaged(false);
}
});
lblErrorMessage.setVisible(false);
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
boolean hasError = !newVal.isEmpty();
errHintPane.setSegment(newVal);
errHintPane.setVisible(hasError);
errHintPane.setManaged(hasError);
hintPane.setVisible(!hasError);
hintPane.setManaged(!hasError);
codeBox.setVisible(!hasError && deviceCode.get() != null);
codeBox.setManaged(!hasError && deviceCode.get() != null);
});
FXUtils.onClicked(codeBox, () -> {
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
runInFX(() -> deviceCode.set(value))
));
HBox linkBox = new HBox();
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
btnAccept.setDisable(false);
detailsPane = new MicrosoftAccountLoginPane(true);
setActions();
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
setActions(lblErrorMessage, actions);
}
detailsContainer.getChildren().add(detailsPane);
@@ -483,9 +399,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
});
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = new JFXButton();
btnAddServer.setGraphic(SVG.ADD.createIcon(20));
btnAddServer.getStyleClass().add("toggle-icon4");
JFXButton btnAddServer = FXUtils.newToggleButton4(SVG.ADD, 20);
btnAddServer.setOnAction(e -> {
Controllers.dialog(new AddAuthlibInjectorServerPane());
});
@@ -650,7 +564,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
}
private static class DialogCharacterSelector extends BorderPane implements CharacterSelector {
public static class DialogCharacterSelector extends JFXDialogLayout implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
@@ -662,17 +576,17 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnAction(e -> latch.countDown());
cancel.getStyleClass().add("dialog-cancel");
listBox.startCategory(i18n("account.choose").toUpperCase(Locale.ROOT));
setCenter(listBox);
setBody(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
setActions(hbox);
onEscPressed(this, cancel::fire);
}
@@ -0,0 +1,320 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import io.nayuki.qrcodegen.QrCode;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.SVGPath;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.QrCodeUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware {
private final Account accountToRelogin;
private final Consumer<AuthInfo> loginCallback;
private final Runnable cancelCallback;
@SuppressWarnings("FieldCanBeLocal")
private final WeakListenerHolder holder = new WeakListenerHolder();
private final ObjectProperty<Step> step = new SimpleObjectProperty<>();
private TaskExecutor browserTaskExecutor;
private TaskExecutor deviceTaskExecutor;
private final JFXButton btnLogin;
private final SpinnerPane loginButtonSpinner;
public MicrosoftAccountLoginPane() {
this(false);
}
public MicrosoftAccountLoginPane(boolean bodyonly) {
this(null, null, null, bodyonly);
}
public MicrosoftAccountLoginPane(Account account, Consumer<AuthInfo> callback, Runnable onCancel, boolean bodyonly) {
this.accountToRelogin = account;
this.loginCallback = callback;
this.cancelCallback = onCancel;
getStyleClass().add("microsoft-login-dialog");
if (bodyonly) {
this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true);
} else {
Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft"));
heading.getStyleClass().add("header-label");
setHeading(heading);
}
this.setMaxWidth(650);
onEscPressed(this, this::onCancel);
btnLogin = new JFXButton(i18n("account.login"));
btnLogin.getStyleClass().add("dialog-accept");
loginButtonSpinner = new SpinnerPane();
loginButtonSpinner.getStyleClass().add("small-spinner-pane");
loginButtonSpinner.setContent(btnLogin);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
btnCancel.setOnAction(e -> onCancel());
setActions(loginButtonSpinner, btnCancel);
holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartAuthorizationCodeLogin)
step.set(new Step.WaitForOpenBrowser(event.getUrl()));
}));
holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> {
if (step.get() instanceof Step.StartDeviceCodeLogin)
step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri()));
}));
this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty()
? new Step.Init()
: new Step.StartAuthorizationCodeLogin());
FXUtils.onChangeAndOperate(step, this::onStep);
}
private void onStep(Step currentStep) {
VBox rootContainer = new VBox(10);
setBody(rootContainer);
rootContainer.setAlignment(Pos.TOP_CENTER);
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
rootContainer.getChildren().add(snapshotHint);
btnLogin.setDisable(true);
loginButtonSpinner.setLoading(false);
return;
}
if (!IntegrityChecker.isOfficial()) {
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
unofficialHintPane.setSegment(i18n("unofficial.hint"));
rootContainer.getChildren().add(unofficialHintPane);
}
if (currentStep instanceof Step.Init) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
var hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("account.methods.microsoft.hint"));
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
loginButtonSpinner.setLoading(false);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.StartDeviceCodeLogin) {
loginButtonSpinner.setLoading(true);
cancelAllTasks();
rootContainer.getChildren().add(new JFXSpinner());
deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE))
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
.executor(true);
} else if (currentStep instanceof Step.WaitForOpenBrowser wait) {
btnLogin.setOnAction(e -> {
FXUtils.openLink(wait.url());
loginButtonSpinner.setLoading(true);
});
loginButtonSpinner.setLoading(false);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setSegment(
i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()),
FXUtils::copyText
);
rootContainer.getChildren().add(hintPane);
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
loginButtonSpinner.setLoading(true);
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
? "https://www.microsoft.com/link?otc=" + wait.userCode()
: wait.verificationUri();
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
StringUtils.escapeXmlAttribute(scanUri),
wait.verificationUri(),
wait.userCode()
));
var qrCode = new SVGPath();
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(scanUri, QrCode.Ecc.MEDIUM)));
qrCode.setScaleX(3);
qrCode.setScaleY(3);
var lblCode = new Label(wait.userCode());
lblCode.getStyleClass().add("code-label");
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
var codeBox = new StackPane(lblCode);
codeBox.getStyleClass().add("code-box");
codeBox.setCursor(Cursor.HAND);
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
codeBox.setMaxWidth(USE_PREF_SIZE);
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
} else if (currentStep instanceof Step.LoginFailed failed) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
cancelAllTasks();
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setSegment(failed.message());
rootContainer.getChildren().add(errHintPane);
}
var linkBox = new FlowPane(8, 8);
linkBox.setAlignment(Pos.CENTER_LEFT);
linkBox.setPrefWrapLength(500);
if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) {
JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device"));
useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin()));
linkBox.getChildren().add(useQrCode);
} else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) {
JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser"));
userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
linkBox.getChildren().add(userBrowser);
}
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
linkBox.getChildren().addAll(profileLink, purchaseLink);
rootContainer.getChildren().add(linkBox);
setBody(rootContainer);
}
private void cancelAllTasks() {
if (browserTaskExecutor != null) browserTaskExecutor.cancel();
if (deviceTaskExecutor != null) deviceTaskExecutor.cancel();
}
private void onCancel() {
cancelAllTasks();
if (cancelCallback != null) cancelCallback.run();
fireEvent(new DialogCloseEvent());
}
private void onLoginCompleted(MicrosoftAccount account, Exception exception) {
if (exception == null) {
if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin);
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
Accounts.setSelectedAccount(account);
if (loginCallback != null) {
try {
loginCallback.accept(account.logIn());
} catch (AuthenticationException e) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e)));
return;
}
}
fireEvent(new DialogCloseEvent());
} else if (!(exception instanceof CancellationException)) {
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception)));
}
}
private sealed interface Step {
final class Init implements Step {
}
final class StartAuthorizationCodeLogin implements Step {
}
record WaitForOpenBrowser(String url) implements Step {
}
final class StartDeviceCodeLogin implements Step {
}
record WaitForScanQrCode(String userCode, String verificationUri) implements Step {
}
record LoginFailed(String message) implements Step {
}
}
}
@@ -1,109 +0,0 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.OAuthAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OAuthAccountLoginDialog extends DialogPane {
private final OAuthAccount account;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
this.account = account;
this.success = success;
this.failed = failed;
setTitle(i18n("account.login.refresh"));
VBox vbox = new VBox(8);
Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
);
} else {
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.hint")
);
}
});
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(usernameLabel, hintPane, box);
setBody(vbox);
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override
protected void onAccept() {
setLoading();
Task.supplyAsync(account::logInWhenCredentialsExpired)
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
if (exception == null) {
success.accept(authInfo);
onSuccess();
} else {
LOG.info("Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
}
@Override
protected void onCancel() {
failed.run();
super.onCancel();
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.function.Consumer;
@@ -100,27 +99,17 @@ public class AdvancedListBox extends ScrollPane {
return add(item);
}
@SuppressWarnings("SuspiciousNameCombination")
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
SVG unselectedGraphic, SVG selectedGraphic) {
AdvancedListItem item = createNavigationDrawerItem(title, null);
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
TransitionPane leftGraphic = new TransitionPane();
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setMouseTransparent(true);
leftGraphic.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setPadding(Insets.EMPTY);
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
FXUtils.onChange(item.activeProperty(), active ->
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
item.setLeftGraphic(leftGraphic);
return add(item);
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
@@ -85,13 +84,11 @@ public class AdvancedListItem extends Control {
}
public void setRightAction(SVG icon, Runnable action) {
var button = new JFXButton();
var button = FXUtils.newToggleButton4(icon, 14);
button.setOnAction(e -> {
action.run();
e.consume();
});
button.getStyleClass().add("toggle-icon4");
button.setGraphic(icon.createIcon(14));
setAlignment(button, Pos.CENTER);
setRightGraphic(button);
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Locale;
import java.util.function.Consumer;
public class HintPane extends VBox {
private final Text label = new Text();
@@ -66,7 +67,11 @@ public class HintPane extends VBox {
}
public void setSegment(String segment) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
this.setSegment(segment, Controllers::onHyperlinkAction);
}
public void setSegment(String segment, Consumer<String> hyperlinkAction) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction));
}
public void setChildren(Node... children) {
@@ -0,0 +1,163 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.ArrayList;
import java.util.List;
/// A custom ImageView with fixed size and corner radius support.
public class ImageContainer extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "image-container";
private final ImageView imageView = new ImageView();
private final Rectangle clip = new Rectangle();
public ImageContainer(double size) {
this(size, size);
}
public ImageContainer(double width, double height) {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
FXUtils.setLimitWidth(this, width);
FXUtils.setLimitHeight(this, height);
imageView.setPreserveRatio(true);
FXUtils.limitSize(imageView, width, height);
StackPane.setAlignment(imageView, Pos.CENTER);
clip.setWidth(width);
clip.setHeight(height);
updateCornerRadius(getCornerRadius());
this.setClip(clip);
this.getChildren().setAll(imageView);
}
private void updateCornerRadius(double radius) {
clip.setArcWidth(radius);
clip.setArcHeight(radius);
}
private static final double DEFAULT_CORNER_RADIUS = 6.0;
private StyleableDoubleProperty cornerRadius;
public StyleableDoubleProperty cornerRadiusProperty() {
if (this.cornerRadius == null) {
cornerRadius = new StyleableDoubleProperty() {
@Override
public Object getBean() {
return ImageContainer.this;
}
@Override
public String getName() {
return "cornerRadius";
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.CORNER_RADIUS;
}
@Override
protected void invalidated() {
updateCornerRadius(get());
}
};
}
return cornerRadius;
}
public double getCornerRadius() {
return cornerRadius == null ? DEFAULT_CORNER_RADIUS : cornerRadius.get();
}
public void setCornerRadius(double radius) {
cornerRadiusProperty().set(radius);
}
public ObjectProperty<Image> imageProperty() {
return imageView.imageProperty();
}
public Image getImage() {
return imageView.getImage();
}
public void setImage(Image image) {
imageView.setImage(image);
}
public BooleanProperty smoothProperty() {
return imageView.smoothProperty();
}
public boolean isSmooth() {
return imageView.isSmooth();
}
public void setSmooth(boolean smooth) {
imageView.setSmooth(smooth);
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return StyleableProperties.STYLEABLES;
}
private static final class StyleableProperties {
private static final CssMetaData<ImageContainer, Number> CORNER_RADIUS =
new CssMetaData<>("-jfx-corner-radius", SizeConverter.getInstance(), DEFAULT_CORNER_RADIUS) {
@Override
public boolean isSettable(ImageContainer control) {
return control.cornerRadius == null || !control.cornerRadius.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(ImageContainer control) {
return control.cornerRadiusProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
styleables.add(CORNER_RADIUS);
STYLEABLES = List.copyOf(styleables);
}
}
}
@@ -28,7 +28,6 @@ import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -40,7 +39,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@DefaultProperty("image")
public final class ImagePickerItem extends BorderPane {
private final ImageView imageView;
private final ImageContainer imageContainer;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
@@ -48,25 +47,20 @@ public final class ImagePickerItem extends BorderPane {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
imageView = new ImageView();
imageView.setSmooth(false);
imageView.setPreserveRatio(true);
imageContainer = new ImageContainer(32);
imageContainer.setSmooth(false);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.EDIT.createIcon(20));
JFXButton selectButton = FXUtils.newToggleButton4(SVG.EDIT, 20);
selectButton.onActionProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.RESTORE.createIcon(20));
JFXButton deleteButton = FXUtils.newToggleButton4(SVG.RESTORE, 20);
deleteButton.onActionProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
FXUtils.installFastTooltip(deleteButton, i18n("button.reset"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
hBox.getChildren().setAll(imageContainer, selectButton, deleteButton);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setSpacing(8);
setRight(hBox);
@@ -78,7 +72,7 @@ public final class ImagePickerItem extends BorderPane {
vBox.setAlignment(Pos.CENTER_LEFT);
setLeft(vBox);
imageView.imageProperty().bind(image);
imageContainer.imageProperty().bind(image);
}
public String getTitle() {
@@ -128,8 +122,4 @@ public final class ImagePickerItem extends BorderPane {
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
}
@@ -31,16 +31,12 @@ import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends HBox {
@@ -72,7 +68,7 @@ public final class MessageDialogPane extends HBox {
private @Nullable ButtonBase cancelButton;
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
this.setSpacing(8);
this.setSpacing(16);
this.getStyleClass().add("jfx-dialog-layout");
Label graphic = new Label();
@@ -219,36 +215,6 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder cancelOnTimeout(long timeoutMs) {
if (dialog.getCancelButton() == null) {
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
}
ButtonBase cancelButton = dialog.getCancelButton();
String originalText = cancelButton.getText();
Timer timer = Lang.getTimer();
timer.scheduleAtFixedRate(new TimerTask() {
long timeout = timeoutMs;
@Override
public void run() {
if (timeout <= 0) {
cancel();
runInFX(() -> {
cancelButton.fire();
});
return;
}
timeout -= 1000;
long currentTimeout = timeout;
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
}
}, 1000, 1000);
return this;
}
public MessageDialogPane build() {
return dialog;
}
@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -33,14 +31,12 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
/// A spinner pane that can show spinner, failed reason, or content.
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
public SpinnerPane() {
getStyleClass().add("spinner-pane");
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public void showSpinner() {
@@ -52,108 +48,195 @@ public class SpinnerPane extends Control {
setLoading(false);
}
public Node getContent() {
return content.get();
private void updateContent() {
if (getSkin() instanceof Skin skin) {
skin.updateContent();
}
}
private ObjectProperty<Node> content;
public ObjectProperty<Node> contentProperty() {
if (content == null)
content = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "content";
}
@Override
protected void invalidated() {
updateContent();
}
};
return content;
}
public void setContent(Node content) {
this.content.set(content);
public Node getContent() {
return contentProperty().get();
}
public boolean isLoading() {
return loading.get();
public void setContent(Node content) {
contentProperty().set(content);
}
private BooleanProperty loading;
public BooleanProperty loadingProperty() {
if (loading == null)
loading = new BooleanPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "loading";
}
@Override
protected void invalidated() {
updateContent();
}
};
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
public boolean isLoading() {
return loading != null && loading.get();
}
public String getFailedReason() {
return failedReason.get();
public void setLoading(boolean loading) {
loadingProperty().set(loading);
}
private StringProperty failedReason;
public StringProperty failedReasonProperty() {
if (failedReason == null)
failedReason = new StringPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "failedReason";
}
@Override
protected void invalidated() {
updateContent();
}
};
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
public String getFailedReason() {
return failedReason != null ? failedReason.get() : null;
}
public void setFailedReason(String failedReason) {
failedReasonProperty().set(failedReason);
}
private ObjectProperty<EventHandler<Event>> onFailedAction;
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
if (onFailedAction == null) {
onFailedAction = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "onFailedAction";
}
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
}
return onFailedAction;
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedAction != null ? onFailedAction.get() : null;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected SkinBase<SpinnerPane> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
Skin(SpinnerPane control) {
super(control);
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onClicked(failedPane, () -> {
EventHandler<Event> action = control.getOnFailedAction();
if (action != null)
action.handle(new Event(FAILED_ACTION));
});
updateContent();
this.getChildren().setAll(root);
}
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
private StackPane contentPane;
private StackPane spinnerPane;
private StackPane failedPane;
private Label failedReasonLabel;
void updateContent() {
SpinnerPane control = getSkinnable();
Node nextContent;
if (control.isLoading()) {
if (spinnerPane == null) {
spinnerPane = new StackPane(new JFXSpinner());
spinnerPane.getStyleClass().add("notice-pane");
}
nextContent = spinnerPane;
} else if (control.getFailedReason() != null) {
if (failedPane == null) {
failedReasonLabel = new Label();
failedPane = new StackPane(failedReasonLabel);
failedPane.getStyleClass().add("notice-pane");
FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
}
failedReasonLabel.setText(control.getFailedReason());
nextContent = failedPane;
} else {
if (contentPane == null) {
contentPane = new StackPane();
}
Node content = control.getContent();
if (content != null)
contentPane.getChildren().setAll(content);
else
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE);
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
nextContent = contentPane;
}
if (nextContent != failedPane && failedReasonLabel != null) {
failedReasonLabel.setText(null);
}
root.setContent(nextContent, ContainerAnimations.FADE);
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -26,12 +25,11 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
@@ -157,8 +155,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected");
private final Color ripplerColor = Color.valueOf("#FFFF8D");
private final HeaderContainer header;
private boolean isSelectingTab = false;
private Tab<?> selectedTab;
@@ -214,7 +210,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(2);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
@@ -593,7 +588,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
private final Tab<?> tab;
private final Label tabText;
private final BorderPane inner;
private final JFXRippler rippler;
public TabHeaderContainer(Tab<?> tab) {
this.tab = tab;
@@ -604,9 +598,8 @@ public class TabHeader extends Control implements TabControl, PageAware {
inner = new BorderPane();
inner.setCenter(tabText);
inner.getStyleClass().add("tab-container");
rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT);
rippler.getStyleClass().add("tab-rippler");
rippler.setRipplerFill(ripplerColor);
inner.setMouseTransparent(true);
RipplerContainer rippler = new RipplerContainer(inner);
getChildren().setAll(rippler);
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -83,8 +82,9 @@ public class TaskExecutorDialogPane extends BorderPane {
setCancel(cancel);
btnCancel.setOnAction(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
if (onCancel.getCancellationAction() != null) {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
}
});
@@ -106,13 +106,18 @@ public final class TaskListPane extends StackPane {
}
@FXThread
private void addStages(@NotNull Collection<String> stages) {
for (String stage : stages) {
stageNodes.computeIfAbsent(stage, s -> {
StageNode node = new StageNode(stage);
private void addStagesHints(@NotNull Collection<Task.StagesHint> hints) {
for (Task.StagesHint hint : hints) {
StageNode node = stageNodes.get(hint.stage());
if (node == null) {
node = new StageNode(hint.stage());
stageNodes.put(hint.stage(), node);
listView.getItems().add(node);
return node;
});
}
for (String stage : hint.aliases()) {
stageNodes.put(stage, node);
}
}
}
@@ -129,7 +134,7 @@ public final class TaskListPane extends StackPane {
Platform.runLater(() -> {
stageNodes.clear();
listView.getItems().clear();
addStages(executor.getStages());
addStagesHints(executor.getHints());
updateProgressNodePadding();
});
}
@@ -138,7 +143,7 @@ public final class TaskListPane extends StackPane {
public void onReady(Task<?> task) {
if (task instanceof Task.StagesHintTask) {
Platform.runLater(() -> {
addStages(((Task<?>.StagesHintTask) task).getStages());
addStagesHints(((Task<?>.StagesHintTask) task).getHints());
updateProgressNodePadding();
});
}
@@ -394,6 +399,8 @@ public final class TaskListPane extends StackPane {
}
private static final class StageNode extends Node {
private int runningTasksCount = 0;
private enum Status {
WAITING(SVG.MORE_HORIZ),
RUNNING(SVG.ARROW_FORWARD),
@@ -454,17 +461,23 @@ public final class TaskListPane extends StackPane {
}
private void begin() {
if (status.get() == Status.WAITING) {
runningTasksCount++;
if (status.get() == Status.WAITING || status.get() == Status.SUCCESS) {
status.set(Status.RUNNING);
}
}
public void fail() {
status.set(Status.FAILED);
public void succeed() {
runningTasksCount = Math.max(0, runningTasksCount - 1);
if (runningTasksCount == 0) {
status.set(Status.SUCCESS);
}
}
public void succeed() {
status.set(Status.SUCCESS);
public void fail() {
runningTasksCount = Math.max(0, runningTasksCount - 1);
status.set(Status.FAILED);
}
public void count() {
@@ -39,6 +39,7 @@ import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
@@ -46,13 +47,10 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class Decorator extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final StringProperty drawerTitle = new SimpleStringProperty();
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onRefreshNavButtonAction = new SimpleObjectProperty<>();
@@ -117,18 +115,6 @@ public class Decorator extends Control {
this.drawerWrapper = drawerWrapper;
}
public ObservableList<Node> getDrawer() {
return drawer.get();
}
public ListProperty<Node> drawerProperty() {
return drawer;
}
public void setDrawer(ObservableList<Node> drawer) {
this.drawer.set(drawer);
}
public ObservableList<Node> getContent() {
return content.get();
}
@@ -153,30 +139,6 @@ public class Decorator extends Control {
this.state.set(state);
}
public String getDrawerTitle() {
return drawerTitle.get();
}
public StringProperty drawerTitleProperty() {
return drawerTitle;
}
public void setDrawerTitle(String drawerTitle) {
this.drawerTitle.set(drawerTitle);
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public ObservableList<Node> getContainer() {
return container.get();
}
@@ -301,7 +263,26 @@ public class Decorator extends Control {
}
public void close() {
onCloseButtonAction.get().run();
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(opacityProperty(), 1, Motion.EASE),
new KeyValue(scaleXProperty(), 1, Motion.EASE),
new KeyValue(scaleYProperty(), 1, Motion.EASE),
new KeyValue(scaleZProperty(), 0.3, Motion.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(opacityProperty(), 0, Motion.EASE),
new KeyValue(scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(scaleZProperty(), 0.8, Motion.EASE)
)
);
timeline.setOnFinished(event -> Launcher.stopApplication());
timeline.play();
} else {
Launcher.stopApplication();
}
}
public void capableDraggingWindow(Node node) {
@@ -20,9 +20,6 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import com.jfoenix.controls.JFXSnackbarLayout;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Insets;
@@ -37,7 +34,6 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
@@ -47,7 +43,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogUtils;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
@@ -81,28 +76,6 @@ public class DecoratorController {
public DecoratorController(Stage stage, Node mainPage) {
decorator = new Decorator(stage);
decorator.setOnCloseButtonAction(() -> {
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.opacityProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.3, Motion.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(decorator.opacityProperty(), 0, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.8, Motion.EASE)
)
);
timeline.setOnFinished(event -> Launcher.stopApplication());
timeline.play();
} else {
Launcher.stopApplication();
}
});
decorator.titleTransparentProperty().bind(config().titleTransparentProperty());
navigator = new Navigator();
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.i18n.I18n;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -50,11 +51,11 @@ public class InstallersPage extends AbstractInstallersPage {
@Override
public String getTitle() {
return group.getGame().versionProperty().get().version();
return ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
}
private String getVersion(String id) {
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
return I18n.getDisplayVersion((RemoteVersion) controller.getSettings().get(id));
}
protected void reload() {
@@ -111,7 +112,7 @@ public class InstallersPage extends AbstractInstallersPage {
}
private void setTxtNameWithLoaders() {
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().version());
StringBuilder nameBuilder = new StringBuilder(getTitle());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
@@ -120,23 +121,22 @@ public class InstallersPage extends AbstractInstallersPage {
}
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName = switch (libraryType) {
case FORGE -> i18n("install.installer.forge");
case NEO_FORGE -> i18n("install.installer.neoforge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric").replace(" ", "_");
case FABRIC -> i18n("install.installer.fabric");
case LITELOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
case OPTIFINE -> i18n("install.installer.optifine");
case FORGE -> "forge";
case NEO_FORGE -> "neoforge";
case CLEANROOM -> "cleanroom";
case LEGACY_FABRIC -> "legacy_fabric";
case FABRIC -> "fabric";
case LITELOADER -> "liteloader";
case QUILT -> "quilt";
case OPTIFINE -> "optifine";
default -> null;
};
if (loaderName == null)
continue;
nameBuilder.append("-").append(loaderName);
if (loaderName != null)
nameBuilder.append('-').append(loaderName);
}
}
@@ -47,6 +47,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
private final Path file;
private final String updateVersion;
private String iconUrl;
private boolean hasSource;
public ModpackInstallWizardProvider(Profile profile) {
this(profile, null, null);
@@ -79,6 +80,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
if (StringUtils.isNotBlank(iconUrl))
settings.put(LocalModpackPage.MODPACK_ICON_URL, iconUrl);
settings.put(ModpackPage.PROFILE, profile);
hasSource = settings.containsKey(LocalModpackPage.MODPACK_FILE) || settings.containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
}
private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
@@ -145,20 +147,30 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
return finishModpackInstallingAsync(settings);
}
private static Node createModpackInstallPage(WizardController controller) {
if (controller.getSettings().containsKey(LocalModpackPage.MODPACK_FILE))
return new LocalModpackPage(controller);
else if (controller.getSettings().containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST))
return new RemoteModpackPage(controller);
else
throw new IllegalArgumentException();
}
@Override
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new ModpackSelectionPage(controller);
case 1:
if (controller.getSettings().containsKey(LocalModpackPage.MODPACK_FILE))
return new LocalModpackPage(controller);
else if (controller.getSettings().containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST))
return new RemoteModpackPage(controller);
else
throw new IllegalArgumentException();
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
if (hasSource) {
return switch (step) {
case 0 -> createModpackInstallPage(controller);
default -> throw new IllegalStateException(
"error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
};
} else {
return switch (step) {
case 0 -> new ModpackSelectionPage(controller);
case 1 -> createModpackInstallPage(controller);
default -> throw new IllegalStateException(
"error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
};
}
}
@@ -139,7 +139,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
}), 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"
@@ -156,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} else {
handler.reject(e.getMessage());
}
}).executor(true),
}),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -40,7 +40,6 @@ import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.zip.ZipException;
@@ -78,21 +77,21 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
// We remove library but not save it,
// so if installation failed will not break down current version.
Task<Version> ret = Task.supplyAsync(() -> version);
List<String> stages = new ArrayList<>();
var hints = new ArrayList<Task.StagesHint>();
for (Object value : settings.asStringMap().values()) {
if (value instanceof RemoteVersion remoteVersion) {
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
hints.add(new Task.StagesHint(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion())));
if ("game".equals(remoteVersion.getLibraryId())) {
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
hints.add(new Task.StagesHint("hmcl.install.libraries"));
hints.add(new Task.StagesHint("hmcl.install.assets"));
}
} else if (value instanceof RemoveVersionAction removeVersionAction) {
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, removeVersionAction.libraryId));
}
}
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHints(hints);
}
@Override
@@ -162,6 +162,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
RemoteVersionListCell(VersionsPage control) {
this.control = control;
imageView.setMouseTransparent(true);
HBox hbox = new HBox(16);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
hbox.setAlignment(Pos.CENTER);
@@ -276,10 +278,12 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(remoteVersion.getGameVersion());
twoLineListItem.addTag(displayGameVersion);
}
}
}
@@ -18,13 +18,13 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
@@ -48,6 +48,8 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -108,23 +110,39 @@ public final class JavaDownloadDialog extends StackPane {
}
private final class DownloadMojangJava extends DialogPane {
private final ToggleGroup toggleGroup = new ToggleGroup();
private final List<JFXCheckBox> checkboxes = new ArrayList<>();
DownloadMojangJava() {
setTitle(i18n("java.download.title"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
setValid(false);
InvalidationListener updateValidStatus = e -> {
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
setValid(true);
return;
}
}
setValid(false);
};
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
JFXCheckBox button = new JFXCheckBox("Java " + version.majorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
if (JavaManager.REPOSITORY.isInstalled(platform, version))
if (JavaManager.REPOSITORY.isInstalled(platform, version)) {
button.setDisable(true);
button.setSelected(true);
} else {
button.selectedProperty().addListener(updateValidStatus);
}
vbox.getChildren().add(button);
checkboxes.add(button);
}
setBody(vbox);
@@ -137,38 +155,52 @@ public final class JavaDownloadDialog extends StackPane {
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
List<GameJavaVersion> selectedVersions = new ArrayList<>();
for (JFXCheckBox box : checkboxes) {
if (!box.isDisabled() && box.isSelected()) {
selectedVersions.add((GameJavaVersion) box.getUserData());
}
}
if (selectedVersions.isEmpty())
return;
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
Task<Void> task = Task.allOf(selectedVersions.stream().map(javaVersion -> JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).wrapResult()).toList())
.whenComplete(Schedulers.javafx(), (results, exception) -> {
Throwable exceptionToDisplay;
if (exception == null) {
List<Throwable> exceptions = results.stream()
.filter(Result::isFailure)
.map(Result::getException)
.map(Lang::resolveException)
.filter(it -> !(it instanceof CancellationException))
.toList();
if (!exceptions.isEmpty()) {
if (exceptions.size() == 1) {
exceptionToDisplay = exceptions.get(0);
} else {
exceptionToDisplay = new IOException("Failed to download Java");
for (Throwable e : exceptions)
exceptionToDisplay.addSuppressed(e);
}
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL);
} else {
exceptionToDisplay = null;
}
} else {
exceptionToDisplay = exception;
}
if (exceptionToDisplay != null) {
LOG.warning("Failed to download java", exceptionToDisplay);
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
}
});
Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL);
}
}
@@ -241,9 +241,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
revealButton.getStyleClass().add("toggle-icon4");
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
revealButton.setOnAction(e -> {
JavaRuntime java = getItem();
if (java != null)
@@ -157,26 +157,20 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
if (skinnable.realPath == null) {
revealButton.setDisable(true);
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(SVG.DELETE.createIcon());
JFXButton removeButton = FXUtils.newToggleButton4(SVG.DELETE);
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
right.getChildren().setAll(revealButton, removeButton);
} else {
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(SVG.RESTORE.createIcon());
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
@@ -64,12 +64,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.util.List;
@@ -320,6 +320,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
Task<?> task = versionList.refreshAsync("")
.thenSupplyAsync(() -> versionList.getVersions("").stream()
.filter(it -> it.getVersionType() == RELEASE)
.filter(it -> NativePatcher.checkSupportedStatus(GameVersionNumber.asGameVersion(it.getGameVersion()), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
.sorted()
.findFirst()
.orElseThrow(() -> new IOException("No versions found")))
@@ -226,9 +226,7 @@ public class PersonalizationPage extends StackPane {
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtLogFontSize)));
JFXButton clearButton = new JFXButton();
clearButton.getStyleClass().add("toggle-icon4");
clearButton.setGraphic(SVG.RESTORE.createIcon());
JFXButton clearButton = FXUtils.newToggleButton4(SVG.RESTORE);
clearButton.setOnAction(e -> cboLogFont.setValue(null));
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
@@ -276,9 +274,7 @@ public class PersonalizationPage extends StackPane {
cboFont.setValue(config().getLauncherFontFamily());
FXUtils.onChange(cboFont.valueProperty(), FontManager::setFontFamily);
JFXButton clearButton = new JFXButton();
clearButton.getStyleClass().add("toggle-icon4");
clearButton.setGraphic(SVG.RESTORE.createIcon());
JFXButton clearButton = FXUtils.newToggleButton4(SVG.RESTORE);
clearButton.setOnAction(e -> cboFont.setValue(null));
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
@@ -140,10 +140,8 @@ public final class SettingsPage extends ScrollPane {
}
{
JFXButton btnUpdate = new JFXButton();
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
btnUpdate.setOnAction(e -> onUpdate());
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.UPDATE.createIcon(20));
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
@@ -60,11 +60,9 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRemove = new JFXButton();
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.CLOSE, 14);
btnRemove.setOnAction(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -551,6 +551,7 @@ public class TerracottaControllerPage extends StackPane {
ComponentSublist locals = new ComponentSublist();
var header = new LinePane();
header.getStyleClass().add("no-padding");
header.setLargeTitle(true);
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
header.setMouseTransparent(true);
@@ -15,6 +15,7 @@ import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -170,6 +171,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
rendererPane.setDescriptionConverter(e -> {
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setItems(Renderer.values());
noJVMArgsPane = new LineToggleButton();
@@ -21,6 +21,7 @@ import javafx.beans.property.BooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -44,13 +45,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
private final Path worldDir;
private final World world;
private final Datapack datapack;
final BooleanProperty readOnly;
public DatapackListPage(WorldManagePage worldManagePage) {
this.worldDir = worldManagePage.getWorld().getFile();
datapack = new Datapack(worldDir.resolve("datapacks"));
world = worldManagePage.getWorld();
datapack = new Datapack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
@@ -68,7 +69,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
private void installSingleDatapack(Path datapack) {
try {
this.datapack.installPack(datapack);
this.datapack.installPack(datapack, world.getGameVersion());
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse datapack file " + datapack, e);
}
@@ -35,7 +35,6 @@ import javafx.scene.Node;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
@@ -50,10 +49,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.Nullable;
@@ -274,20 +270,20 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageView.setImage(image);
imageContainer.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
@@ -296,7 +292,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return;
}
}
imageView.setImage(image);
imageContainer.setImage(image);
}, Schedulers.javafx());
}
@@ -304,7 +300,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView();
ImageContainer imageContainer = new ImageContainer(32);
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
@@ -320,13 +316,10 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
checkBox.disableProperty().bind(isReadOnlyProperty);
imageView.setFitWidth(32);
imageView.setFitHeight(32);
imageView.setPreserveRatio(true);
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
StackPane.setMargin(container, new Insets(8));
container.getChildren().setAll(checkBox, imageView, content);
container.getChildren().setAll(checkBox, imageContainer, content);
getContainer().getChildren().setAll(container);
getContainer().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(this, mouseEvent));
@@ -341,7 +334,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty);
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
}
}
@@ -17,8 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.*;
@@ -32,7 +34,6 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
@@ -48,10 +49,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.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -532,28 +530,29 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private static final Insets PADDING = new Insets(9, 9, 0, 9);
private final RipplerContainer graphic;
private final StackPane wrapper = new StackPane();
private final TwoLineListItem content = new TwoLineListItem();
private final ImageView imageView = new ImageView();
private final ImageContainer imageContainer = new ImageContainer(40);
{
setPadding(PADDING);
HBox container = new HBox(8);
container.getStyleClass().add("card");
container.setPadding(new Insets(8));
container.setCursor(Cursor.HAND);
container.setAlignment(Pos.CENTER_LEFT);
JFXDepthManager.setDepth(container, 1);
imageView.setFitWidth(40);
imageView.setFitHeight(40);
imageContainer.setMouseTransparent(true);
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
container.getChildren().setAll(imageContainer, content);
HBox.setHgrow(content, Priority.ALWAYS);
this.graphic = new RipplerContainer(container);
graphic.setPosition(JFXRippler.RipplerPos.FRONT);
FXUtils.onClicked(graphic, () -> {
wrapper.getChildren().setAll(this.graphic);
wrapper.getStyleClass().add("card-no-padding");
FXUtils.onClicked(wrapper, () -> {
RemoteMod item = getItem();
if (item != null)
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getProfileVersion(), getSkinnable().callback));
@@ -579,8 +578,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
if (getSkinnable().shouldDisplayCategory(category))
content.addTag(getSkinnable().getLocalizedCategory(category));
}
iconLoader.load(imageView.imageProperty(), item.getIconUrl());
setGraphic(graphic);
iconLoader.load(imageContainer.imageProperty(), item.getIconUrl());
setGraphic(wrapper);
}
}
});
@@ -32,8 +32,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
@@ -225,25 +223,18 @@ public class DownloadPage extends Control implements DecoratorPage {
VBox pane = new VBox(8);
pane.getStyleClass().add("gray-background");
pane.setPadding(new Insets(10));
ScrollPane scrollPane = new ScrollPane(pane);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
HBox descriptionPane = new HBox(8);
descriptionPane.setMinHeight(Region.USE_PREF_SIZE);
descriptionPane.setAlignment(Pos.CENTER);
pane.getChildren().add(descriptionPane);
descriptionPane.getStyleClass().add("card-non-transparent");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
{
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
var imageContainer = new ImageContainer(40);
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
imageContainer.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
}
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
descriptionPane.getChildren().add(imageContainer);
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
@@ -286,8 +277,13 @@ public class DownloadPage extends Control implements DecoratorPage {
spinnerPane.setOnFailedAction(e -> getSkinnable().loadModVersions());
ComponentList list = new ComponentList();
StackPane.setAlignment(list, Pos.TOP_CENTER);
spinnerPane.setContent(list);
ScrollPane scrollPane = new ScrollPane(list);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.setOverflowHidden(scrollPane, 8);
StackPane.setAlignment(scrollPane, Pos.TOP_CENTER);
spinnerPane.setContent(scrollPane);
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
if (control.versions == null) return;
@@ -341,7 +337,7 @@ public class DownloadPage extends Control implements DecoratorPage {
});
}
getChildren().setAll(scrollPane);
getChildren().setAll(pane);
}
}
@@ -362,10 +358,8 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
var imageView = new ImageContainer(40);
pane.getChildren().setAll(imageView, content);
RipplerContainer container = new RipplerContainer(pane);
FXUtils.onClicked(container, () -> {
@@ -493,6 +487,7 @@ public class DownloadPage extends Control implements DecoratorPage {
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.smoothScrolling(scrollPane);
FXUtils.setOverflowHidden(scrollPane, 8);
spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -27,32 +25,26 @@ import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final ImageView imageView;
private final ImageContainer imageContainer;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings("SuspiciousNameCombination")
public GameAdvancedListItem() {
this.imageView = new ImageView();
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setPreserveRatio(true);
imageView.setImage(null);
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
imageView.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
setLeftGraphic(imageViewWrapper);
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
imageContainer.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
}
private void loadVersion(String version) {
@@ -68,11 +60,11 @@ public class GameAdvancedListItem extends AdvancedListItem {
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
setTitle(i18n("version.manage.manage"));
setSubtitle(version);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
imageContainer.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(VersionIconType.DEFAULT.getIcon());
imageContainer.setImage(VersionIconType.DEFAULT.getIcon());
}
}
}
@@ -25,7 +25,6 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.ListCell;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -42,7 +41,7 @@ public final class GameListCell extends ListCell<GameListItem> {
private final Region graphic;
private final ImageView imageView;
private final ImageContainer imageView;
private final TwoLineListItem content;
private final JFXRadioButton chkSelected;
@@ -87,8 +86,7 @@ public final class GameListCell extends ListCell<GameListItem> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
this.imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
this.imageView = new ImageContainer(32);
this.content = new TwoLineListItem();
BorderPane.setAlignment(content, Pos.CENTER);
@@ -108,30 +106,26 @@ public final class GameListCell extends ListCell<GameListItem> {
right.setAlignment(Pos.CENTER_RIGHT);
this.btnUpgrade = new JFXButton();
this.btnUpgrade = FXUtils.newToggleButton4(SVG.UPDATE);
btnUpgrade.setOnAction(e -> {
GameListItem item = this.getItem();
if (item != null)
item.update();
});
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.UPDATE.createIcon());
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
right.getChildren().add(btnUpgrade);
this.btnLaunch = new JFXButton();
this.btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH);
btnLaunch.setOnAction(e -> {
GameListItem item = this.getItem();
if (item != null)
item.testGame();
});
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
right.getChildren().add(btnLaunch);
this.btnManage = new JFXButton();
this.btnManage = FXUtils.newToggleButton4(SVG.MORE_VERT);
btnManage.setOnAction(e -> {
GameListItem item = this.getItem();
if (item == null)
@@ -141,9 +135,7 @@ public final class GameListCell extends ListCell<GameListItem> {
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(SVG.MORE_VERT.createIcon());
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
right.getChildren().add(btnManage);
}
@@ -17,21 +17,34 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
@@ -39,9 +52,14 @@ import org.jackhuang.hmcl.ui.profile.ProfilePage;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
@@ -60,8 +78,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
});
selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class);
GameList gameList = new GameList();
{
ScrollPane pane = new ScrollPane();
VBox.setVgrow(pane, Priority.ALWAYS);
@@ -85,13 +101,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame)
.addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack)
.addNavigationDrawerItem(i18n("button.refresh"), SVG.REFRESH, gameList::refreshList)
.addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2);
setLeft(pane, bottomLeftCornerList);
}
setCenter(gameList);
setCenter(new GameList());
}
public ObjectProperty<Profile> selectedProfileProperty() {
@@ -122,7 +137,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
private static class GameList extends ListPageBase<GameListItem> {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final ObservableList<GameListItem> sourceList = FXCollections.observableArrayList();
private final FilteredList<GameListItem> filteredList = new FilteredList<>(sourceList);
public GameList() {
setItems(filteredList);
Profiles.registerVersionsListener(this::loadVersions);
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
@@ -133,44 +153,125 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
listenerHolder.clear();
setLoading(true);
setFailedReason(null);
if (profile != Profiles.getSelectedProfile())
return;
ObservableList<GameListItem> children = FXCollections.observableList(profile.getRepository().getDisplayVersions()
.map(instance -> new GameListItem(profile, instance.getId()))
.toList());
setItems(children);
if (children.isEmpty()) {
List<GameListItem> versionItems = profile.getRepository().getDisplayVersions().map(instance -> new GameListItem(profile, instance.getId())).toList();
sourceList.setAll(versionItems);
if (versionItems.isEmpty()) {
setFailedReason(i18n("version.empty.hint"));
}
setLoading(false);
}
private Predicate<GameListItem> createPredicate(String searchText) {
if (searchText == null || searchText.isEmpty()) {
return item -> true;
}
if (searchText.startsWith("regex:")) {
String regex = searchText.substring("regex:".length());
try {
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
return item -> pattern.matcher(item.id).find();
} catch (PatternSyntaxException e) {
return item -> false;
}
} else {
return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT));
}
}
public void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
@Override
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
protected Skin<?> createDefaultSkin() {
return new GameListSkin(this);
}
private class GameListSkin extends ToolbarListPageSkin<GameListItem, GameList> {
private static class GameListSkin extends SkinBase<GameList> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
private final HBox toolbarNormal;
public GameListSkin() {
super(GameList.this);
private final JFXTextField searchField;
public GameListSkin(GameList skinnable) {
super(skinnable);
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<GameListItem> listView = new JFXListView<>();
{
toolbarPane = new TransitionPane();
searchBar = new HBox();
toolbarNormal = new HBox();
searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
changeToolbar(toolbarNormal);
searchField.clear();
});
onEscPressed(searchField, closeSearchBar::fire);
searchBar.getChildren().setAll(searchField, closeSearchBar);
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
root.getContent().add(toolbarPane);
}
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
center.failedReasonProperty().bind(skinnable.failedReasonProperty());
listView.setCellFactory(x -> new GameListCell());
listView.setItems(skinnable.getItems());
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
center.setContent(listView);
root.getContent().add(center);
}
pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
@Override
protected ListCell<GameListItem> createListCell(JFXListView<GameListItem> listView) {
return new GameListCell();
private void changeToolbar(HBox newToolbar) {
Node oldToolbar = toolbarPane.getCurrentNode();
if (newToolbar != oldToolbar) {
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
if (newToolbar == searchBar) {
runInFX(searchField::requestFocus);
}
}
}
}
}
}
@@ -30,7 +30,6 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
@@ -38,6 +37,7 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
@@ -87,7 +87,7 @@ public final class GameListPopupMenu extends StackPane {
private final Region graphic;
private final ImageView imageView;
private final ImageContainer imageView;
private final TwoLineListItem content;
private final StringProperty tag = new SimpleStringProperty();
@@ -103,8 +103,7 @@ public final class GameListPopupMenu extends StackPane {
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
this.imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
this.imageView = new ImageContainer(32);
imageViewContainer.getChildren().setAll(imageView);
this.content = new TwoLineListItem();
@@ -50,7 +50,9 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
}
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
page.supportChinese.set(false);
return page;
}
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
@@ -252,7 +252,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
Controllers.navigateForward(new ModUpdatesPage(modManager, result));
}
})
.withStagesHint(Collections.singletonList("update.checking")),
.withStagesHints("update.checking"),
i18n("mods.check_updates"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(instanceId)) {
@@ -30,7 +30,6 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
@@ -377,20 +376,20 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
}
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageView.setImage(image);
imageContainer.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageContainer.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
@@ -400,7 +399,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
}
imageView.setImage(image);
imageContainer.setImage(image);
}, Schedulers.javafx());
}
}
@@ -414,9 +413,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 40, 40);
modInfo.loadIcon(imageView, null);
var imageContainer = new ImageContainer(40);
modInfo.loadIcon(imageContainer, null);
TwoLineListItem title = new TwoLineListItem();
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
@@ -437,7 +435,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
title.setSubtitle(subtitle.toString());
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
titleContainer.getChildren().setAll(imageContainer, title);
setHeading(titleContainer);
Label description = new Label(modInfo.getModInfo().getDescription().toString());
@@ -554,11 +552,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView();
ImageContainer imageContainer = new ImageContainer(24);
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = new JFXButton();
JFXButton infoButton = new JFXButton();
JFXButton revealButton = new JFXButton();
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
BooleanProperty booleanProperty;
Tooltip warningTooltip;
@@ -575,23 +573,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
content.setMouseTransparent(true);
setSelectable();
imageView.setFitWidth(24);
imageView.setFitHeight(24);
imageView.setPreserveRatio(true);
imageView.setImage(VersionIconType.COMMAND.getIcon());
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(SVG.RESTORE.createIcon());
imageContainer.setImage(VersionIconType.COMMAND.getIcon());
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(SVG.FOLDER.createIcon());
infoButton.getStyleClass().add("toggle-icon4");
infoButton.setGraphic(SVG.INFO.createIcon());
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
container.getChildren().setAll(checkBox, imageContainer, content, restoreButton, revealButton, infoButton);
StackPane.setMargin(container, new Insets(8));
getContainer().getChildren().setAll(container);
@@ -616,7 +602,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ModLoaderType modLoaderType = modInfo.getModLoaderType();
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
String displayName = modInfo.getName();
if (modTranslations != null && I18n.isUseChinese()) {
@@ -8,7 +8,6 @@ import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -19,10 +18,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream;
@@ -181,10 +177,10 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
}
private static final class ResourcepackListCell extends MDListCell<ResourcepackInfoObject> {
private final ImageView imageView = new ImageView();
private final ImageContainer imageView = new ImageContainer(32);
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = new JFXButton();
private final JFXButton btnDelete = new JFXButton();
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
private final JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
private final ResourcepackListPage page;
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, ResourcepackListPage page) {
@@ -198,7 +194,6 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
HBox left = new HBox(8);
left.setAlignment(Pos.CENTER);
FXUtils.limitSize(imageView, 32, 32);
left.getChildren().add(imageView);
left.setPadding(new Insets(0, 8, 0, 0));
FXUtils.setLimitWidth(left, 48);
@@ -207,12 +202,6 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
HBox.setHgrow(content, Priority.ALWAYS);
root.setCenter(content);
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
HBox right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
right.getChildren().setAll(btnReveal, btnDelete);
@@ -22,17 +22,14 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
@@ -444,9 +441,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
if (image == null) {
return super.getIcon(size);
} else {
ImageView imageView = new ImageView();
imageView.setFitHeight(size);
imageView.setFitWidth(size);
var imageView = new ImageContainer(size);
imageView.setImage(image);
return imageView;
}
@@ -550,9 +545,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final TwoLineListItem center;
private final HBox right;
private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;
private final ImageContainer iconImageView;
private final SVGContainer iconSVGView;
private final Tooltip tooltip = new Tooltip();
@@ -565,18 +559,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.left = new StackPane();
left.setPadding(new Insets(0, 8, 0, 0));
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);
this.iconSVG = new SVGPath();
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);
this.iconImageView = new ImageContainer(32);
this.iconSVGView = new SVGContainer(32);
BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
@@ -591,19 +575,15 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem))
item.onReveal();
});
JFXButton btnDelete = new JFXButton();
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
btnDelete.setOnAction(event -> {
Item item = getItem();
if (item != null && !(item instanceof BackItem)) {
@@ -638,8 +618,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
iconSVGView.setIcon(item.getIcon());
left.getChildren().setAll(iconSVGView);
}
center.setTitle(item.getName());
@@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -61,6 +63,13 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private String preferredVersionName = null;
public static class WorkingDirChangedEvent extends Event {
public static final EventType<WorkingDirChangedEvent> EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED");
public WorkingDirChangedEvent() {
super(EVENT_TYPE);
}
}
public VersionPage() {
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
@@ -74,6 +83,21 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> {
if (this.version.get() != null) {
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(getProfile(), getVersion());
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(getProfile(), getVersion());
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(getProfile(), getVersion());
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(getProfile(), getVersion());
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(getProfile(), getVersion());
}
});
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
}
@@ -34,8 +34,12 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -48,7 +52,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
@@ -99,6 +102,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final InvalidationListener usesGlobalListener;
private final ChangeListener<Boolean> specificSettingsListener;
private final InvalidationListener javaListener = any -> initJavaSubtitle();
private final ChangeListener<GameDirectoryType> gameDirTypeListener = (ob, o, n) -> fireWorkingDirChanged();
private final ChangeListener<String> gameDirListener = (ob, o, n) -> fireWorkingDirChanged();
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
@@ -455,13 +462,20 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
else
profile.getRepository().globalizeVersionSetting(versionId);
Platform.runLater(() -> loadVersion(profile, versionId));
FXUtils.runInFX(() -> {
loadVersion(profile, versionId);
fireWorkingDirChanged();
});
};
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
componentList.disableProperty().bind(enableSpecificSettings.not());
}
private void fireWorkingDirChanged() {
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
}
@Override
public void loadVersion(Profile profile, String versionId) {
@@ -513,6 +527,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting.defaultJavaPathPropertyProperty().removeListener(javaListener);
lastVersionSetting.javaVersionProperty().removeListener(javaListener);
lastVersionSetting.gameDirTypeProperty().removeListener(gameDirTypeListener);
lastVersionSetting.gameDirProperty().removeListener(gameDirListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().unbind();
@@ -589,6 +606,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> profile.getRepository().getRunDirectory(versionId).toAbsolutePath().normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
versionSetting.gameDirTypeProperty().addListener(gameDirTypeListener);
versionSetting.gameDirProperty().addListener(gameDirListener);
lastVersionSetting = versionSetting;
initJavaSubtitle();
@@ -727,7 +747,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
private static List<String> getSupportedResolutions() {
@@ -103,7 +103,7 @@ public final class Versions {
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
}
}).executor(true),
}),
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
@@ -26,7 +26,6 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -35,6 +34,7 @@ import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
@@ -232,9 +232,8 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
ImageView imageView = new ImageView();
var imageView = new ImageContainer(32);
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
@@ -255,18 +254,14 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
right.getChildren().add(btnReveal);
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnReveal.setOnAction(event -> skinnable.onReveal());
JFXButton btnDelete = new JFXButton();
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE);
right.getChildren().add(btnDelete);
FXUtils.installFastTooltip(btnDelete, i18n("world.backup.delete"));
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE.createIcon());
btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::onDelete, null));
}
@@ -30,7 +30,6 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -69,7 +68,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
private final World world;
private CompoundTag levelDat;
ImageView iconImageView = new ImageView();
private final ImageContainer iconImageView = new ImageContainer(32);
public WorldInfoPage(WorldManagePage worldManagePage) {
this.worldManagePage = worldManagePage;
@@ -137,14 +136,12 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
iconPane.setTitle(i18n("world.icon"));
{
FXUtils.limitSize(iconImageView, 32, 32);
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
JFXButton editIconButton = new JFXButton();
JFXButton resetIconButton = new JFXButton();
JFXButton editIconButton = FXUtils.newToggleButton4(SVG.EDIT, 20);
JFXButton resetIconButton = FXUtils.newToggleButton4(SVG.RESTORE, 20);
{
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
editIconButton.setDisable(isReadOnly);
editIconButton.setOnAction(event -> Controllers.confirm(
I18n.i18n("world.icon.change.tip"),
@@ -154,13 +151,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
null
));
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
editIconButton.getStyleClass().add("toggle-icon4");
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
resetIconButton.setDisable(isReadOnly);
resetIconButton.setOnAction(event -> this.clearWorldIcon());
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
resetIconButton.getStyleClass().add("toggle-icon4");
}
HBox hBox = new HBox(8);
@@ -30,7 +30,6 @@ import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -243,7 +242,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
private final WorldListPage page;
private final RipplerContainer graphic;
private final ImageView imageView;
private final ImageContainer imageView;
private final Tooltip leftTooltip;
private final TwoLineListItem content;
private final JFXButton btnLaunch;
@@ -262,9 +261,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
this.imageView = new ImageView();
this.imageView = new ImageContainer(32);
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
}
{
@@ -278,12 +276,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
btnLaunch = new JFXButton();
btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH);
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
right.getChildren().add(btnLaunch);
btnLaunch.getStyleClass().add("toggle-icon4");
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
btnLaunch.setOnAction(event -> {
World world = getItem();
@@ -291,10 +287,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
page.launch(world);
});
JFXButton btnMore = new JFXButton();
JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT);
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
btnMore.setOnAction(event -> {
World world = getItem();
if (world != null)
@@ -20,20 +20,12 @@ package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -103,9 +95,6 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
if (showCrashWindow) {
new CrashWindow(report).show();
}
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(report);
}
}
});
} catch (Throwable handlingException) {
@@ -115,22 +104,4 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
FileSaver.shutdown();
LOG.shutdown();
}
private void reportToServer(CrashReport crashReport) {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
map.put("crash_report", crashReport.getDisplayText());
map.put("version", Metadata.VERSION);
map.put("log", LOG.getLogs());
try {
String response = NetworkUtils.doPost(URI.create(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
LOG.error("Crash server response: " + response);
} catch (IOException ex) {
LOG.error("Unable to post HMCL server.", ex);
}
});
t.setDaemon(true);
t.start();
}
}
@@ -236,7 +236,11 @@ public final class NativePatcher {
minVersion = "1.6";
} else if (platform.equals(Platform.LINUX_RISCV64)) {
minVersion = "1.8";
maxVersion = "1.21.5";
if (gameVersion.compareTo("1.21.5") > 0 && gameVersion.compareTo("26.1-snapshot-8") < 0) {
// LWJGL version mismatch
return SupportStatus.UNSUPPORTED;
}
} else if (platform.equals(Platform.LINUX_LOONGARCH64)) {
minVersion = "1.6";
} else if (platform.equals(Platform.LINUX_LOONGARCH64_OW)) {
@@ -0,0 +1,48 @@
/*
* 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.util;
import io.nayuki.qrcodegen.QrCode;
/// @author Glavo
public final class QrCodeUtils {
public static String toSVGPath(QrCode qr) {
return toSVGPath(qr, 0);
}
public static String toSVGPath(QrCode qr, int border) {
int actualSize = qr.size + border * 2;
var builder = new StringBuilder(qr.size * qr.size * 12);
builder.append('M').append(actualSize).append(' ').append(actualSize).append("ZM0 0Z");
for (int y = 0; y < qr.size; y++) {
for (int x = 0; x < qr.size; x++) {
if (qr.getModule(x, y)) {
if (x != 0 || y != 0)
builder.append(' ');
builder.append("M").append(x + border).append(',').append(y + border).append("h1v1h-1z");
}
}
}
return builder.toString();
}
private QrCodeUtils() {
}
}
@@ -79,10 +79,11 @@ public final class SelfDependencyPatcher {
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest digest = DigestUtils.getDigest("SHA-1");
private SelfDependencyPatcher() throws IncompatibleVersionException {
private SelfDependencyPatcher() throws PatchException {
// We can only self-patch JavaFX on specific platform.
if (dependencies == null) {
throw new IncompatibleVersionException();
throw new PatchException("Unsupported platform: operating system %s, architecture %s".formatted(
System.getProperty("os.name"), System.getProperty("os.arch")));
}
final String customUrl = System.getProperty("hmcl.openjfx.repo");
@@ -159,18 +160,12 @@ public final class SelfDependencyPatcher {
/**
* Patch in any missing dependencies, if any.
*/
public static void patch() throws PatchException, IncompatibleVersionException, CancellationException {
public static void patch() throws PatchException, CancellationException {
// Do nothing if JavaFX is detected
try {
try {
Class.forName("javafx.application.Application");
return;
} catch (Exception ignored) {
}
} catch (UnsupportedClassVersionError error) {
// Loading the JavaFX class was unsupported.
// We are probably on 8 and its on 11
throw new IncompatibleVersionException();
Class.forName("javafx.application.Application");
return;
} catch (Exception ignored) {
}
SelfDependencyPatcher patcher = new SelfDependencyPatcher();
@@ -378,14 +373,15 @@ public final class SelfDependencyPatcher {
}
public static class PatchException extends Exception {
PatchException(String message) {
super(message);
}
PatchException(String message, Throwable cause) {
super(message, cause);
}
}
public static class IncompatibleVersionException extends Exception {
}
public static class ProgressFrame extends JDialog {
private final JProgressBar progressBar;
+108 -50
View File
@@ -532,6 +532,7 @@
}
.jfx-layout-heading {
-fx-font-weight: BOLD;
-fx-font-size: 20.0px;
-fx-alignment: center-left;
-fx-padding: 5.0 0.0 5.0 0.0;
@@ -572,6 +573,41 @@
-fx-text-fill: -monet-on-surface;
}
/*******************************************************************************
* *
* Microsoft Login Dialog *
* *
******************************************************************************/
.microsoft-login-dialog:bodyonly {
-fx-padding: 0px 0px 0px 0px;
}
.method-title {
-fx-text-fill: -monet-on-surface;
-fx-font-weight: bold;
}
.method-desc {
-fx-text-fill: -monet-outline;
-fx-font-size: 0.9em;
-fx-line-spacing: 2;
-fx-wrap-text: true;
-fx-text-alignment: center;
}
.code-box {
-fx-background-color: -monet-surface-variant;
-fx-background-radius: 6;
-fx-padding: 0 10 0 10;
-fx-alignment: center;
}
.code-box .code-label {
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-text-fill: -monet-primary;
}
/*******************************************************************************
* *
@@ -755,31 +791,39 @@
* *
******************************************************************************/
.svg-slider .thumb {
-fx-stroke: -monet-on-surface;
-fx-fill: -monet-on-surface;
.jfx-slider .track,
.jfx-slider:vertical .track {
-fx-background-color: -monet-secondary-container;
-fx-background-radius: 5;
-fx-background-insets: 0;
-fx-pref-width: 2px;
-fx-pref-height: 2px;
}
.jfx-slider:disabled {
-fx-opacity: 0.4;
}
/*******************************************************
* *
* For the main demo sliders *
* *
*******************************************************/
.jfx-slider {
-jfx-indicator-position: right;
}
.jfx-slider .track {
-fx-background-color: -monet-on-surface-variant-transparent-38;
}
.jfx-slider .thumb {
.jfx-slider .thumb,
.jfx-slider:focused .thumb {
-fx-background-color: -monet-primary;
-fx-background-radius: 20;
-fx-background-insets: 0;
}
.jfx-slider .colored-track {
-fx-background-color: -monet-primary;
-fx-background-radius: 5 0 0 5;
-fx-background-insets: 0;
}
.jfx-slider .slider-value {
-fx-stroke: -monet-inverse-on-surface;
-fx-font-size: 10;
}
.jfx-slider .animated-thumb {
-fx-pref-width: 30px;
-fx-pref-height: 30px;
-fx-background-color: -monet-inverse-surface;
-fx-background-radius: 50% 50% 50% 0%;
-fx-background-insets: 0;
}
/*******************************************************************************
@@ -868,23 +912,28 @@
* *
*******************************************************************************/
.jfx-progress-bar {
-fx-pref-width: 500.0;
}
.jfx-progress-bar > .track, .jfx-progress-bar > .bar {
-fx-background-radius: 0;
-fx-background-insets: 0;
}
.jfx-progress-bar > .track {
-fx-background-color: -monet-secondary-container;
}
.jfx-progress-bar > .bar {
.jfx-progress-bar > .bar,
.jfx-progress-bar:indeterminate > .bar{
-fx-background-color: -monet-primary;
-fx-padding: 1.5;
}
.jfx-progress-bar > .secondary-bar,
.jfx-progress-bar:indeterminate > .secondary-bar {
-fx-background-color: -monet-secondary;
}
.jfx-progress-bar > .track,
.jfx-progress-bar > .bar {
-fx-background-radius: 0;
-fx-background-insets: 0;
}
/*******************************************************************************
* *
* JFX Textfield *
@@ -1016,6 +1065,13 @@
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.card-no-padding {
-fx-background-color: -monet-surface-container-low-transparent-80;
-fx-background-radius: 4;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.card-non-transparent {
-fx-background-color: -monet-surface-container-low;
-fx-background-radius: 4;
@@ -1054,10 +1110,6 @@
-fx-font-size: 12;
}
.options-list-item > .line-component .line-component-container {
-fx-padding: 10 16 10 16;
}
.options-list-item .svg {
-fx-fill: -monet-on-surface;
-fx-border-color: -monet-outline-variant;
@@ -1400,21 +1452,19 @@
* *
*******************************************************************************/
.jfx-spinner > .arc {
-fx-stroke-width: 3.0;
-fx-fill: transparent;
.jfx-spinner .arc {
-fx-stroke: -monet-primary-container;
}
.jfx-spinner .arc {
-fx-stroke-line-cap: round;
.jfx-spinner:determinate .percentage {
-fx-fill: -monet-on-surface-variant;
}
.first-spinner {
-jfx-radius: 20;
}
.first-spinner > .arc {
.first-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1422,7 +1472,7 @@
-jfx-radius: 10;
}
.small-spinner > .arc {
.small-spinner .arc {
-fx-stroke-width: 3.0;
}
@@ -1430,7 +1480,7 @@
-jfx-radius: 10;
}
.small-spinner-pane .jfx-spinner > .arc {
.small-spinner-pane .jfx-spinner .arc {
-fx-stroke-width: 3.0;
}
@@ -1438,7 +1488,7 @@
-jfx-radius: 30;
}
.second-spinner > .arc {
.second-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1446,7 +1496,7 @@
-jfx-radius: 40;
}
.third-spinner > .arc {
.third-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1454,7 +1504,7 @@
-jfx-radius: 50;
}
.fourth-spinner > .arc {
.fourth-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1462,7 +1512,7 @@
-jfx-radius: 60;
}
.fifth-spinner > .arc {
.fifth-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1470,7 +1520,7 @@
-jfx-radius: 70;
}
.sixth-spinner > .arc {
.sixth-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1478,7 +1528,7 @@
-jfx-radius: 80;
}
.seventh-spinner > .arc {
.seventh-spinner .arc {
-fx-stroke-width: 5.0;
}
@@ -1887,6 +1937,14 @@
* *
******************************************************************************/
.line-component .line-component-container {
-fx-padding: 10 16 10 16;
}
.line-component.no-padding .line-component-container {
-fx-padding: 0;
}
.line-component:large-title .title-label {
-fx-font-size: 15px;
}
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -21,7 +21,7 @@
about=About
about.copyright=Copyright
about.copyright.statement=Copyright © 2026 huangyuhui
about.copyright.statement=Copyright © 2013-2026 huangyuhui and contributors.
about.author=Author
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -118,11 +118,11 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
account.methods.microsoft.logging_in=Logging in...
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
\n\
If the website fails to load, please open %s manually in your browser.\n\
\n\
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
account.methods.microsoft.methods.device=Log In with QR Code
account.methods.microsoft.methods.device.hint=Scan QR code or visit <a href="%s">%s</a> to complete login, enter <b>%s</b> in the opened page.
account.methods.microsoft.methods.browser=Log In via Browser
account.methods.microsoft.methods.browser.hint=Click the "Log in" button or <a href="%s">copy the link</a> and paste it into the browser to log in.
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
account.methods.microsoft.profile=Account Profile
account.methods.microsoft.purchase=Buy Minecraft
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
@@ -141,7 +141,6 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play
\n\
This option is for advanced users only. We do not recommend changing this option unless you know what you are doing.
account.methods.offline.uuid.malformed=Invalid format.
account.methods.forgot_password=Forgot Password
account.methods.ban_query=Ban Query
account.missing=No Accounts
account.missing.add=Click here to add one.
@@ -368,7 +367,6 @@ download.provider.balanced.desc=Balanced, but may not be the latest
download.provider.mirror=From Mirror
download.provider.mirror.desc=Fast, but may not be the latest
download.java=Downloading Java
download.java.override=This Java version already exists. Do you want to uninstall and reinstall it?
download.java.process=Java Download Process
download.javafx=Downloading dependencies for the launcher...
download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\
@@ -403,9 +401,6 @@ extension.schematic=Schematic File
extension.world=World Archive
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
fatal.javafx.incompatible=Missing JavaFX environment.\n\
Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\
Please update your Java to version 11 or later.
fatal.javafx.incomplete=The JavaFX environment is incomplete.\n\
Please try replacing your Java or reinstalling OpenJFX.
fatal.javafx.missing=Missing JavaFX environment. Please open Hello Minecraft! Launcher with Java, which includes OpenJFX.
@@ -447,7 +442,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
\n\
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe (Software)" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
@@ -478,7 +473,7 @@ game.crash.reason.analyzing=Analyzing...
game.crash.reason.multiple=Several reasons detected:\n\n
game.crash.reason.block=The game crashed because of a block in the world.\n\
\n\
You can try removing this block using MCEdit or deleting the mod that added it.\n\
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Block Type: %1$s\n\
Location: %2$s
@@ -494,7 +489,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
Each mod can only be installed once. Please delete the duplicate mod and try again.
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
\n\
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Entity Type: %1$s\n\
Location: %2$s
@@ -1370,9 +1365,13 @@ settings.advanced.post_exit_command=Post-exit Command
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
settings.advanced.renderer=Renderer
settings.advanced.renderer.default=Default
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (Software)
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
settings.advanced.server_ip=Server Address
settings.advanced.server_ip.prompt=Automatically join after launching the game
settings.advanced.unsupported_system_options=Settings not applicable to the current system
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -21,7 +21,6 @@
about=حول
about.copyright=حقوق الطبع والنشر
about.copyright.statement=حقوق النشر © 2026 huangyuhui
about.author=المطور
about.author.statement=bilibili @huanghongxun
about.claim=اتفاقية الترخيص
@@ -120,7 +119,6 @@ account.methods.offline.uuid.hint=UUID هو معرف فريد للاعبي Minec
\n\
هذا الخيار للمستخدمين المتقدمين فقط. لا نوصي بتغيير هذا الخيار ما لم تعرف ما تفعله.
account.methods.offline.uuid.malformed=تنسيق غير صالح.
account.methods.forgot_password=نسيت كلمة المرور
account.methods.ban_query=استعلام الحظر
account.missing=لا توجد حسابات
account.missing.add=انقر هنا لإضافة واحد.
@@ -341,7 +339,6 @@ download.provider.official=من المصادر الرسمية
download.provider.balanced=من الأسرع المتاح
download.provider.mirror=من المرآة
download.java=تنزيل Java
download.java.override=هذا الإصدار من Java موجود بالفعل. هل تريد إلغاء التثبيت وإعادة التثبيت؟
download.java.process=عملية تنزيل Java
download.javafx=تنزيل التبعيات للمشغل...
download.javafx.notes=نحن نقوم حالياً بتنزيل تبعيات HMCL من الإنترنت.\n\
@@ -372,9 +369,6 @@ extension.mod=ملف التعديل
extension.world=أرشيف العالم
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher لا يمكنه إنشاء دليل HMCL (%s). يرجى نقل HMCL إلى موقع آخر وإعادة فتحه.
fatal.javafx.incompatible=بيئة JavaFX مفقودة.\n\
Hello Minecraft! Launcher لا يمكنه تثبيت JavaFX تلقائياً على Java <11.\n\
يرجى تحديث Java الخاص بك إلى الإصدار 11 أو أحدث.
fatal.javafx.incomplete=بيئة JavaFX غير مكتملة.\n\
يرجى محاولة استبدال Java الخاص بك أو إعادة تثبيت OpenJFX.
fatal.javafx.missing=بيئة JavaFX مفقودة. يرجى فتح Hello Minecraft! Launcher باستخدام Java، والذي يتضمن OpenJFX.
@@ -416,7 +410,7 @@ fatal.samba=إذا فتحت Hello Minecraft! Launcher من محرك شبكة Sam
fatal.illegal_char=يحتوي مسار المستخدم الخاص بك على حرف غير قانوني "=". لن تتمكن من استخدام authlib-injector أو تغيير مظهر حسابك غير المتصل.
fatal.unsupported_platform=Minecraft غير مدعوم بالكامل على منصتك حتى الآن، لذلك قد تواجه ميزات مفقودة أو حتى تكون غير قادر على تشغيل اللعبة.\n\
\n\
إذا لم تتمكن من تشغيل Minecraft 1.17 وما بعده، يمكنك محاولة تبديل "المُصيِّر" إلى "Mesa LLVMpipe (برمجيات)" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام عرض CPU لتوافق أفضل.
إذا لم تتمكن من تشغيل Minecraft 1.17 وما بعده، يمكنك محاولة تبديل "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام عرض CPU لتوافق أفضل.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher وفر دعماً لمنصة Loongson.\n\
إذا واجهت مشاكل عند لعب اللعبة، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على المساعدة.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher وفر دعماً لمنصة Apple silicon، باستخدام Java ARM الأصلي لتشغيل الألعاب للحصول على تجربة ألعاب أكثر سلاسة.\n\
@@ -444,12 +438,6 @@ game.crash.info=معلومات التعطل
game.crash.reason=سبب التعطل
game.crash.reason.analyzing=جاري التحليل...
game.crash.reason.multiple=تم اكتشاف عدة أسباب:\n\n
game.crash.reason.block=تعطلت اللعبة بسبب كتلة في العالم.\n\
\n\
يمكنك محاولة إزالة هذه الكتلة باستخدام MCEdit أو حذف التعديل الذي أضافها.\n\
\n\
نوع الكتلة: %1$s\n\
الموقع: %2$s
game.crash.reason.bootstrap_failed=تعطلت اللعبة بسبب التعديل "%1$s".\n\
\n\
يمكنك محاولة حذفه أو تحديثه.
@@ -460,12 +448,6 @@ game.crash.reason.duplicated_mod=لا يمكن للعبة الاستمرار ف
%2$s\n\
\n\
يمكن تثبيت كل تعديل مرة واحدة فقط. يرجى حذف التعديل المكرر والمحاولة مرة أخرى.
game.crash.reason.entity=تعطلت اللعبة بسبب كيان في العالم.\n\
\n\
يمكنك محاولة إزالة الكيان باستخدام MCEdit أو حذف التعديل الذي أضافه.\n\
\n\
نوع الكيان: %1$s\n\
الموقع: %2$s
game.crash.reason.modmixin_failure=تعطلت اللعبة لأن بعض التعديلات فشلت في الحقن.\n\
\n\
هذا يعني عموماً أن التعديل به خطأ أو غير متوافق مع البيئة الحالية.\n\
@@ -1278,9 +1260,13 @@ settings.advanced.post_exit_command=أمر ما بعد الخروج
settings.advanced.post_exit_command.prompt=الأوامر التي سيتم تنفيذها بعد خروج اللعبة
settings.advanced.renderer=المُصيِّر
settings.advanced.renderer.default=الافتراضي
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (برمجيات)
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيف)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=برمجيات (أداء ضعيف، أفضل توافق)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
settings.advanced.server_ip=عنوان الخادم
settings.advanced.server_ip.prompt=الانضمام تلقائيًا بعد تشغيل اللعبة
settings.advanced.unsupported_system_options=إعدادات غير قابلة للتطبيق على النظام الحالي
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -21,7 +21,6 @@
about=Acerca de
about.copyright=Copyright
about.copyright.statement=Copyright © 2026 huangyuhui.
about.author=Autor
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -123,7 +122,6 @@ account.methods.offline.uuid.hint=UUID es un identificador único para los jugad
\n\
Esta opción es sólo para usuarios avanzados. No recomendamos editar esta opción a menos que sepas lo que estás haciendo.
account.methods.offline.uuid.malformed=Formato no válido.
account.methods.forgot_password=Olvidé mi contraseña
account.methods.ban_query=Consulta de bloqueo de cuentas
account.missing=No hay cuentas
account.missing.add=Haga clic aquí para añadir una.
@@ -346,7 +344,6 @@ download.provider.official=De fuentes oficiales
download.provider.balanced=De la fuente más rápida disponible
download.provider.mirror=Desde espejo
download.java=Descargando Java
download.java.override=Esta versión de Java ya existe. ¿Desea desinstalarla y volver a instalarla?
download.java.process=Proceso de descarga Java
download.javafx=Descargando dependencias para el launcher...
download.javafx.notes=Estamos descargando dependencias para HMCL desde Internet.\n\
@@ -374,9 +371,6 @@ extension.mod=Archivo mod
extension.world=Archivo del mundo
fatal.create_hmcl_current_directory_failure=El lanzador no puede crear el directorio HMCL (%s). Por favor, mueva el lanzador a otra ubicación y vuelva a abrirlo.
fatal.javafx.incompatible=No se encontró un entorno JavaFX.\n\
Hello Minecraft! Launcher no puede instalar automáticamente JavaFX con versiones de Java inferiores a la 11.\n\
Por favor, actualice su Java a la versión 11 o posterior.
fatal.javafx.incomplete=El entorno JavaFX está incompleto.\n\
Por favor, intente reemplazar su Java o reinstalar OpenJFX.
fatal.javafx.missing=No se encontró un entorno JavaFX.\n\
@@ -421,7 +415,7 @@ fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
fatal.unsupported_platform=Minecraft aún no es totalmente compatible con tu plataforma, por lo que es posible que falten funciones o incluso que no puedas iniciar el juego.\n\
\n\
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe (Software)» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher ha prestado apoyo a la plataforma Loongson.\n\
Si tienes problemas al jugar, puedes visitar https://docs.hmcl.net/groups.html para obtener ayuda.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher ha proporcionado soporte para la plataforma de chips de Apple, utilizando Java nativo de ARM para ejecutar juegos y conseguir una experiencia de juego más fluida.\n\
@@ -450,12 +444,6 @@ game.crash.info=Información de colgado
game.crash.reason=Motivo del colgado
game.crash.reason.analyzing=Analizando...
game.crash.reason.multiple=Se han detectado varias razones:\n\n
game.crash.reason.block=El juego se ha colgado debido a un bloqueo en el mundo.\n\
\n\
Puedes intentar eliminar este bloque usando MCEdit o borrando el mod que lo añadió.\n\
\n\
Tipo de bloque: %1$s\n\
Ubicación: %2$s
game.crash.reason.bootstrap_failed=El juego se ha colgado debido al mod «%1$s».\n\
\n\
Puedes intentar borrarlo o actualizarlo.
@@ -469,12 +457,6 @@ game.crash.reason.duplicated_mod=El juego no puede iniciarse debido a la existen
%2$s\n\
\n\
Cada mod sólo puede instalarse una vez, por favor, elimine el mod duplicado e inténtelo de nuevo.
game.crash.reason.entity=El juego se ha colgado debido a una entidad en el mundo.\n\
\n\
Puedes intentar eliminar la entidad usando MCEdit o borrando el mod que la añadió.\n\
\n\
Tipo de entidad: %1$s\n\
Ubicación: %2$s
game.crash.reason.fabric_version_0_12=La versión de Fabric Loader 0.12 o posterior es incompatible con los mods instalados actualmente. Necesitas bajarlo a la versión 0.11.7.
game.crash.reason.fabric_warnings=El cargador de Fabric Loader ha advertido:\n\
\n\
@@ -1286,10 +1268,14 @@ settings.advanced.process_priority.high=Alta
settings.advanced.post_exit_command=Comando post-salida
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
settings.advanced.renderer=Renderizador
settings.advanced.renderer.default=Predeterminado
settings.advanced.renderer.default=Por defecto
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (Software)
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
settings.advanced.server_ip=Dirección del servidor
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,6 @@
about=について
about.copyright=著作権
about.copyright.statement=Copyright © 2026 huangyuhui
about.author=著者
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -91,7 +90,6 @@ account.methods.microsoft.birth=誕生日の設定を編集する方法...
account.methods.microsoft.deauthorize=アカウントのバインドを解除
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
account.methods.microsoft.logging_in=ログイン...
account.methods.forgot_password=パスワードをお忘れの方
account.methods.microsoft.makegameidsettings=プロファイルを作成/プロフィール名を編集する
account.methods.microsoft.profile=アカウントプロファイル..
account.methods.microsoft.purchase=Minecraftを購入する
@@ -323,7 +321,6 @@ extension.datapack=Datapack
extension.mod=Modファイル
extension.world=World zip
fatal.javafx.incompatible=アプリケーションは11未満の現在のJava環境でJavaFXにパッチを適用できません。\nこのアプリは、JDK11以降またはJavaFXがバンドルされたJDKを使用して実行してください。
fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用している場合は、Oracle JRE 8にダウングレードするか、BellSoft Liberica FullJREをインストールしてください。\n他のOpenJDKビルドを使用している場合は、OpenJFXが含まれていることを確認してください。
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
fatal.migration_requires_manual_reboot=更新が完了しました。Hello Minecraftを再開してください!ランチャー。
@@ -332,7 +329,7 @@ fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may
fatal.illegal_char=ユーザーフォルダーのパスに不正な文字'='が含まれています, ログインアカウントやオフラインログインではスキンの変更ができなくなり。
fatal.unsupported_platform=現在、お使いのプラットフォームでは Minecraft が完全にはサポートされていないため、機能が欠けたり、ゲームを起動できない場合があります。\n\
\n\
Minecraft 1.17 以降を起動できない場合は、「グローバル/インスタンス固有の設定 → 詳細設定」で「レンダラー」を「Mesa LLVMpipe (ソフト)」に切り替え、CPU レンダリングを使用することで互換性が向上する可能性があります。
Minecraft 1.17 以降を起動できない場合は、「グローバル/インスタンス固有の設定 → 詳細設定」で「レンダラー」を「Mesa LLVMpipe」に切り替え、CPU レンダリングを使用することで互換性が向上する可能性があります。
file=ファイル
@@ -349,13 +346,11 @@ game.crash.info=ゲームステータス
game.crash.reason=クラッシュアナライザー
game.crash.reason.analyzing=分析中..
game.crash.reason.multiple=複数の理由が検出されました:\n\n
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s\nブロックの場所:%2$s
game.crash.reason.bootstrap_failed=mod %1$s がクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod %1$s は構成ファイル %2$s を解析できません。
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
game.crash.reason.duplicated_mod=Mod %1$s が重複してインストールされているため、現在のゲームを継続して実行できません。\n%2$s\n各Modは1つしかインストールできませんので、余分なModを削除して再度お試しください。
game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用できないため、現在のゲームを続行できません。\n問題を解決するために、このモジュールを削除または更新してみてください。
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s\nエンティティの場所:%2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
@@ -797,9 +792,13 @@ settings.advanced.post_exit_command=終了後のコマンド
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
settings.advanced.renderer=レンダラー
settings.advanced.renderer.default=デフォルト
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (ソフト)
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
settings.advanced.server_ip=サーバーアドレス
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,6 @@
about=有涉
about.copyright=持權
about.copyright.statement=© 2026 huangyuhui 持權
about.author=作者
about.author.statement=嗶站 @huanghongxun
about.claim=客契
@@ -110,7 +109,6 @@ account.methods.microsoft.logging_in=方登入……
account.methods.microsoft.makegameidsettings=添檔 / 書檔之名
account.methods.microsoft.profile=纂戶簿訊息
account.methods.microsoft.purchase=買礦藝
account.methods.forgot_password=亡符節
account.methods.ban_query=檢戶簿羈否
account.methods.microsoft.snapshot.website=官網
account.methods.offline=離綫之式
@@ -345,7 +343,6 @@ download.provider.balanced.desc=均,容有舊
download.provider.mirror=先別源
download.provider.mirror.desc=至快,容有舊
download.java=引爪哇
download.java.override=爪哇是版既現,除而復裝諸?
download.java.process=引爪哇
download.javafx=甫引行需諸案……
download.javafx.notes=方引 HMCL 所需諸案。\n擊「迭引源」以閲詳,并迭之。擊「罷」以止而退。\n誡:引之允滯,宜迭其源。
@@ -370,7 +367,6 @@ extension.mod=改囊案
extension.world=生界緊囊
fatal.create_hmcl_current_directory_failure=HMCL 不能立其案夾 (%s),宜稍遷而再啟之。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.javafx.incompatible=JavaFX 闕。\n既行於爪哇之次乎爪哇十一者,HMCL 不能自補之也。宜晉爪哇於十一抑後之者。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.javafx.incomplete=JavaFX 損。宜更爪哇,抑復置 OpenJFX。
fatal.javafx.missing=JavaFX 闕。君須行 HMCL 於爪哇之俻 OpenJFX 者。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.config_change_owner_root=君方啟 HMCL 以根戶簿,是則他戶簿或不能啟之。\n君可求助於 https://docs.hmcl.net/help.html。\n猶啟諸?
@@ -387,7 +383,7 @@ fatal.deprecated_java_version.update=HMCL 之晉也,將須爪哇十七與新
fatal.samba=誠行於案夾之同享以 Samba 者,啟者或不能行。宜新爪哇,抑遷於自案。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.illegal_char=君之案夾名有諱文「=」,是以外載之戶簿、離綫戶簿之更外觀者弗行。\n君可求助於 https://docs.hmcl.net/help.html。
fatal.unsupported_platform=礦藝未盡適君之算機,是以戲事或損,至於不能啟。\n\
礦藝一點一七以晉之不能啟,或於「(全例/例殊) 戲設 → 進階置設 → 勘誤置設」改「繪製之器」以「Mesa LLVMpipe (軟體)」,或能兼之。\n君可求助於右上之鈕。
礦藝一點一七以晉之不能啟,或於「(全例/例殊) 戲設 → 進階置設 → 勘誤置設」改「繪製之器」以「Mesa LLVMpipe」,或能兼之。\n君可求助於右上之鈕。
fatal.unsupported_platform.loongarch=HMCL 既適龍芯。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.macos_arm64=HMCL 既適蘋矽。宜啟以 ARM 之爪哇,以益君之戲事。\n誠有謬,宜啟以 x86-64 之爪哇,以益其兼。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.windows_arm64=HMCL 既適 ARM 之視窗。誠有謬,宜啟以 x86 之爪哇。\n誠用<b>栝柑</b>,或須置<a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">開圖庫兼囊</a>,而後可戯。擊鏈以置之於微軟貨舍。\n君可求助於右上之鈕。
@@ -410,14 +406,14 @@ game.crash.feedback=<b>無錄是頁以示人!</b>宜擊<b>「導崩記」</b>
game.crash.info=戲訊
game.crash.reason=潰退之由
game.crash.reason.analyzing=方求之焉……
game.crash.reason.block=塊謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s
game.crash.reason.block=塊謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n塊類:%1$s\n塊位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
game.crash.reason.bootstrap_failed=改囊「%1$s」謬而崩。\n或抑廢之。
game.crash.reason.mixin_apply_mod_failed=「%1$s」不能旁嵌而崩。\n或廢之,或新之。
game.crash.reason.config=置設之案謬而崩。\n改囊「%1$s」之置設「%2$s」有謬。
game.crash.reason.multiple=其所由也衆:\n\n
game.crash.reason.debug_crash=手崩也。\n戲事無誤,乃君致之!
game.crash.reason.duplicated_mod=改囊「%1$s」復而崩。\n%2$s\n改囊不能再載。宜廢冗者。
game.crash.reason.entity=實體謬而崩。\n或操 <a href="https://www.mcedit.net">MCEdit</a> 修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s
game.crash.reason.entity=實體謬而崩。\n或操輿圖纂具修檔以去之,抑徑去改囊之有干者。\n實體類:%1$s\n實體位:%2$s\n\n輿圖纂具薦:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英吉利語大典</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">漢語大典</a>
game.crash.reason.fabric_version_0_12=改囊或弗見容於緞載之〇點一二與愈晉者。宜退乎〇點一一點七。
game.crash.reason.fabric_warnings=緞報曰:\n%1$s
game.crash.reason.file_already_exists=案「%1$s」既存而謬。\n誠以爲可去,宜另備而去之,然後復行。
@@ -1084,9 +1080,13 @@ settings.advanced.post_exit_command=令於戲訖
settings.advanced.post_exit_command.prompt=將後戲訖而行
settings.advanced.renderer=繪器
settings.advanced.renderer.default=本
settings.advanced.renderer.default.desc=開圖庫
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (軟繪器)
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
settings.advanced.server_ip=伺服器之址
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
settings.advanced.unsupported_system_options=不逮今之械網所選
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,6 @@
about=О лаунчере
about.copyright=Авторские права
about.copyright.statement=Авторские права © 2026 huangyuhui.
about.author=Автор
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -122,7 +121,6 @@ account.methods.offline.name.invalid=Для имени пользователя
account.methods.offline.uuid=UUID
account.methods.offline.uuid.hint=UUID - это уникальный идентификатор игрового персонажа в Minecraft. Способ генерации UUID различается в разных лаунчерах игр. Изменение UUID на тот, который генерируется другим лаунчером гарантирует, что игровые блоки/предметы в рюкзаке вашего офлайн аккаунта останутся. Этот параметр предназначен для экспертов. Если вы не знаете что делаете, мы не советуем вам изменять этот параметр. Эта опция не требуется для присоединения к серверам.
account.methods.offline.uuid.malformed=Недопустимый формат.
account.methods.forgot_password=Забыли пароль
account.methods.ban_query=Просмотреть аккаунт заблокирован
account.missing=Нет аккаунтов
account.missing.add=Нажмите здесь, чтобы добавить.
@@ -343,7 +341,6 @@ download.provider.official=Из официальных источников
download.provider.balanced=Из самых быстрых доступных
download.provider.mirror=Из зеркала
download.java=Скачивание Java
download.java.override=Эта версия Java уже существует. Вы хотите удалить и установить ее заново?
download.java.process=Процесс скачивания Java
download.javafx=Скачивание зависимостей лаунчера...
download.javafx.notes=Скачивание зависимостей для лаунчер из Интернета.\n\
@@ -374,9 +371,6 @@ extension.mod=Файл мода
extension.world=Архив мира
fatal.create_hmcl_current_directory_failure=Лаунчер не может создать папку HMCL (%s). Пожалуйста, переместите лаунчер в другое место и откройте его снова.
fatal.javafx.incompatible=Отсутствует среда JavaFX.\n\
Лаунчер не может автоматически установить JavaFX на Java <11.\n\
Обновите Java до версии 11 или более поздней.
fatal.javafx.incomplete=Среда JavaFX является неполной.\n\
Попробуйте заменить Java или переустановить OpenJFX.
fatal.javafx.missing=Отсутствует среда JavaFX.\n\
@@ -415,7 +409,7 @@ fatal.illegal_char=Недопустимый символ «=» в пути к п
Вы не сможете использовать аккаунт authlib-injector или изменить скин для аккаунта в режиме офлайн.
fatal.unsupported_platform=Minecraft еще не полностью поддерживается на вашей платформе, поэтому вы можете столкнуться с отсутствием функций или даже не сможете запустить игру.\n\
\n\
Если вы не можете запустить Minecraft версии 1.17 и новее, попробуйте переключить «Рендерер» на «Mesa LLVMpipe (Программный)» в разделе «Глобальные/настройки экземпляра → Расширенные настройки», чтобы использовать рендеринг через CPU для лучшей совместимости.
Если вы не можете запустить Minecraft версии 1.17 и новее, попробуйте переключить «Рендерер» на «Mesa LLVMpipe» в разделе «Глобальные/настройки экземпляра → Расширенные настройки», чтобы использовать рендеринг через CPU для лучшей совместимости.
fatal.unsupported_platform.loongarch=Лаунчер обеспечил поддержку платформы Loongson.\n\
Если у вас возникнут проблемы во время игры, вы можете обратиться за помощью на сайт https://docs.hmcl.net/groups.html.
fatal.unsupported_platform.macos_arm64=Лаунчер обеспечил поддержку платформы Apple silicon, используя родную ARM Java для запуска игр, чтобы получить более плавный игровой опыт.\n\
@@ -444,12 +438,6 @@ game.crash.info=Сведения об отказе
game.crash.reason=Анализатор сбоев
game.crash.reason.analyzing=Анализирование...
game.crash.reason.multiple=Обнаружено несколько причин:\n\n
game.crash.reason.block=Игра отказала из-за блока в мире.\n\
\n\
Удалите этот блок с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип блока: %1$s\n\
Расположение блока: %2$s
game.crash.reason.bootstrap_failed=Игра отказала из-за мода «%1$s».\n\
\n\
Попробуйте удалить или обновить его.
@@ -460,12 +448,6 @@ game.crash.reason.duplicated_mod=Не удалось продолжить зап
%2$s\n\
\n\
Каждый мод должен быть установлен только один, удалите дублирующий мод и попробуйте снова.
game.crash.reason.entity=Игра отказала из-за сущность в мире.\n\
\n\
Удалите эту сущность с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип сущность: %1$s\n\
Расположение сущность: %2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12 (и выше) несовместим с установленными модами. Следует понизить версию fabric до 0.11.7.
game.crash.reason.mixin_apply_mod_failed=Игра отказала из-за невозможности применить mixin к моду «%1$s».\n\
\n\
@@ -1278,9 +1260,13 @@ settings.advanced.post_exit_command=Команда после выхода
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (Программный)
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
settings.advanced.server_ip=Адрес сервера
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,6 @@
about=Про програму
about.copyright=Авторське право
about.copyright.statement=Авторське право © 2026 huangyuhui
about.author=Автор
about.author.statement=bilibili @huanghongxun
about.claim=Ліцензійна угода з кінцевим користувачем (EULA)
@@ -120,7 +119,6 @@ account.methods.offline.uuid.hint=UUID - це унікальний іденти
\n\
Ця опція призначена лише для досвідчених користувачів. Ми не рекомендуємо змінювати цю опцію, якщо ви не знаєте, що робите.
account.methods.offline.uuid.malformed=Недійсний формат.
account.methods.forgot_password=Забули пароль
account.methods.ban_query=Запит блокування
account.missing=Немає облікових записів
account.missing.add=Натисніть тут, щоб додати.
@@ -342,7 +340,6 @@ download.provider.official=З офіційних джерел
download.provider.balanced=З найшвидшого доступного
download.provider.mirror=З дзеркала
download.java=Завантаження Java
download.java.override=Ця версія Java вже існує. Бажаєте видалити та перевстановити її?
download.java.process=Процес завантаження Java
download.javafx=Завантаження залежностей для лаунчера...
download.javafx.notes=Ми зараз завантажуємо залежності для HMCL з Інтернету. \n\
@@ -372,9 +369,6 @@ extension.mod=Файл мода
extension.world=Архів світу
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Лаунчер не може створити каталог HMCL (%s). Перемістіть HMCL в інше місце та відкрийте його знову.
fatal.javafx.incompatible=Відсутнє середовище JavaFX. \n\
Hello Minecraft! Лаунчер не може автоматично встановити JavaFX на Java <11. \n\
Оновіть вашу Java до версії 11 або новішої.
fatal.javafx.incomplete=Середовище JavaFX неповне. \n\
Спробуйте замінити вашу Java або перевстановити OpenJFX.
fatal.javafx.missing=Відсутнє середовище JavaFX. Відкрийте Hello Minecraft! Лаунчер за допомогою Java, яка включає OpenJFX.
@@ -401,7 +395,7 @@ fatal.samba=Якщо ви відкрили Hello Minecraft! Лаунчер з м
fatal.illegal_char=Ваш шлях користувача містить недопустимий символ "=". Ви не зможете використовувати authlib-injector або змінювати скін вашого автономного облікового запису.
fatal.unsupported_platform=Minecraft ще не повністю підтримується на вашій платформі, тому можливі відсутні функції або навіть неможливість запуску гри.\n\
\n\
Якщо ви не можете запустити Minecraft версії 1.17 і новіші, спробуйте змінити «Рендерер» на «Mesa LLVMpipe (Програмний)» у розділі «Глобальні/налаштування екземпляра → Розширені налаштування», щоб використовувати рендеринг через CPU для кращої сумісності.
Якщо ви не можете запустити Minecraft версії 1.17 і новіші, спробуйте змінити «Рендерер» на «Mesa LLVMpipe» у розділі «Глобальні/налаштування екземпляра → Розширені налаштування», щоб використовувати рендеринг через CPU для кращої сумісності.
fatal.unsupported_platform.loongarch=Hello Minecraft! Лаунчер надав підтримку платформи Loongson.\n\
Якщо у вас виникли проблеми під час гри, ви можете відвідати https://docs.hmcl.net/groups.html для отримання допомоги.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Лаунчер надав підтримку платформи Apple silicon, використовуючи нативну ARM Java для запуску ігор для отримання плавнішого ігрового досвіду.\n\
@@ -429,12 +423,6 @@ game.crash.info=Інформація про збій
game.crash.reason=Причина збою
game.crash.reason.analyzing=Аналіз...
game.crash.reason.multiple=Виявлено кілька причин:\n\n
game.crash.reason.block=Гра зазнала збою через блок у світі. \n\
\n\
Ви можете спробувати видалити цей блок за допомогою MCEdit або видалити мод, який його додав. \n\
\n\
Тип блоку: %1$s \n\
Місцезнаходження: %2$s
game.crash.reason.bootstrap_failed=Гра зазнала збою через мод "%1$s". \n\
\n\
Ви можете спробувати видалити або оновити його.
@@ -443,10 +431,6 @@ game.crash.reason.debug_crash=Гра зазнала збою, оскільки
game.crash.reason.duplicated_mod=Гра не може продовжувати роботу через дублікати модів "%1$s". \n\
%2$s \n\
Кожен мод можна встановити лише один раз. Видаліть дублікат мода та спробуйте знову.
game.crash.reason.entity=Гра зазнала збою через сутність у світі. \n\
Ви можете спробувати видалити сутність за допомогою MCEdit або видалити мод, який її додав. \n\
Тип сутності: %1$s \n\
Місцезнаходження: %2$s
game.crash.reason.modmixin_failure=Гра зазнала збою, оскільки деякі моди не вдалося ввести. \n\
Це, як правило, означає, що мод має помилку або несумісний з поточним середовищем. \n\
Ви можете перевірити журнал, щоб знайти помилковий мод.
@@ -1223,9 +1207,13 @@ settings.advanced.post_exit_command=Команда після виходу
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (Програмний)
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
settings.advanced.server_ip=Адреса сервера
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,7 @@
about=關於
about.copyright=著作權
about.copyright.statement=著作權所有 © 2026 huangyuhui
about.copyright.statement=著作權所有 © 2013-2026 huangyuhui 與貢獻者
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=使用者協議
@@ -30,7 +30,7 @@ about.legal=法律宣告
about.thanks_to=鳴謝
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載來源。請贊助支援 BMCLAPI!
about.thanks_to.burningtnt.statement=為 HMCL 貢獻許多技術支援
about.thanks_to.contributors=所有透過 Issue、Pull Request 等管道參與本項目的貢獻者
about.thanks_to.contributors=所有透過 Issue、Pull Request 等管道參與本專案的貢獻者
about.thanks_to.contributors.statement=沒有開源社群的支援,HMCL 無法走到今天
about.thanks_to.gamerteam.statement=提供預設背景圖
about.thanks_to.glavo.statement=負責 HMCL 的日常維護
@@ -104,7 +104,7 @@ account.methods=登入方式
account.methods.authlib_injector=外部登入
account.methods.microsoft=Microsoft 帳戶
account.methods.microsoft.birth=如何變更帳戶出生日期
account.methods.microsoft.code=代碼 (已自動複製)
account.methods.microsoft.code=程式碼 (已自動複製)
account.methods.microsoft.close_page=已完成 Microsoft 帳戶授權。啟動器將自動執行後續步驟。你現在可以關閉本頁面了。
account.methods.microsoft.deauthorize=移除應用存取權
account.methods.microsoft.error.add_family=請點擊 <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">此處</a> 更改你的帳戶出生日期,使年齡滿 18 歲以上,或將帳戶加入到家庭中。
@@ -117,12 +117,13 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros
account.methods.microsoft.logging_in=登入中……
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
account.methods.microsoft.methods.device=掃描 QR Code 登入
account.methods.microsoft.methods.device.hint=掃描 QR Code 或訪問 <a href="%s">%s</a>,在開啟的頁面中輸入 <b>%s</b> 完成登入。
account.methods.microsoft.methods.browser=在瀏覽器中登入
account.methods.microsoft.methods.browser.hint=點擊「登入」按鈕或者<a href="%s">複製連結</a>並在瀏覽器中貼上以登入。
account.methods.microsoft.manual=<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>
account.methods.microsoft.profile=編輯帳戶個人資訊
account.methods.microsoft.purchase=購買 Minecraft
account.methods.forgot_password=忘記密碼
account.methods.ban_query=查詢帳戶是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 進行登入。
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。請下載官方版本來重新整理帳戶。
@@ -362,7 +363,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mirror=盡量使用鏡像源
download.provider.mirror.desc=載入快,但可能不是最新
download.java=下載 Java
download.java.override=此 Java 版本已經存在,是否移除並重新安裝?
download.java.process=下載 Java
download.javafx=正在下載必要的執行時元件
download.javafx.notes=正在透過網路下載 HMCL 必要的執行時元件。\n點擊「切換下載源」按鈕查看詳情以及選取下載源。點擊「取消」按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切換下載源。
@@ -393,7 +393,6 @@ extension.schematic=原理圖檔案
extension.world=世界壓縮檔
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
fatal.javafx.incompatible=缺少 JavaFX 執行環境。\nHMCL 無法在低於 Java 11 的 Java 環境上自行補全 JavaFX 執行環境。請更新到 Java 11 或更高版本。
fatal.javafx.incomplete=JavaFX 執行環境不完整。請嘗試更換你的 Java 或者重新安裝 OpenJFX。
fatal.javafx.missing=缺少 JavaFX 執行環境。請使用包含 OpenJFX 的 Java 執行環境開啟 Hello Minecraft! Launcher。
fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft! Launcher,這可能導致你未來無法使用其他帳戶正常開啟 HMCL。\n是否繼續開啟?
@@ -411,7 +410,7 @@ fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe (軟體繪製器)」,以獲得更好的相容性。
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
@@ -435,13 +434,13 @@ game.crash.info=遊戲訊息
game.crash.reason=崩潰原因
game.crash.reason.analyzing=分析中……
game.crash.reason.multiple=檢測到多個原因:\n\n
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
@@ -480,7 +479,7 @@ game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
game.crash.reason.forge_repeat_installation=目前遊戲由於 Forge 重複安裝,無法繼續執行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳並回報至 GitHub,以便我們找到更多線索並修復此問題。\n目前你可以在「實例管理 → 自動安裝」中移除 Forge 並重新安裝。
game.crash.reason.optifine_repeat_installation=目前遊戲由於重複安裝 OptiFine,無法繼續執行。\n請刪除模組目錄下的 OptiFine 或前往「實例管理 → 自動安裝」移除安裝的 OptiFine。
game.crash.reason.optifine_is_not_compatible_with_forge=目前遊戲由於 OptiFine 與目前版本的 Forge 不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲,或在「實例管理 → 自動安裝」中更換版本。\n經測試,Forge 版本過高或過低都可能導致崩潰。
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 倉庫</a>。
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 儲存庫</a>。
game.crash.reason.mod_files_are_decompressed=目前遊戲由於模組檔案被解壓了,無法繼續執行。\n請直接把整個模組檔案放進模組目錄中即可。\n解壓模組檔案會導致遊戲出錯。請刪除模組目錄中已被解壓的模組,然後再啟動遊戲。
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=目前遊戲由於你所安裝的模組過多,超出了遊戲的 ID 限制,無法繼續執行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修正模組,或刪除部分大型模組。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=目前遊戲可能由於 OptiFine 而無法繼續執行。\n該問題只在特定 OptiFine 版本中出現,你可以嘗試在「實例管理 → 自動安裝」中更換 OptiFine 的版本。
@@ -692,6 +691,7 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=確認
modpack=模組包
modpack.choose=選取要安裝的遊戲模組包檔案
@@ -863,7 +863,7 @@ mods.add.failed=新增模組「%s」失敗。
mods.add.success=成功新增模組「%s」。
mods.add.title=選取要新增的模組檔案
mods.broken_dependency.title=損壞的相依模組
mods.broken_dependency.desc=該相依模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源。
mods.broken_dependency.desc=該相依模組曾經存在於模組儲存庫中,但現在已被刪除,請嘗試其他下載源。
mods.category=類別
mods.channel.alpha=Alpha
mods.channel.beta=Beta
@@ -885,7 +885,7 @@ mods.dependency.required=必需相依模組 (必須單獨下載,缺少可能
mods.dependency.tool=相依庫 (必須單獨下載,缺少可能會導致遊戲無法啟動)
mods.dependency.include=內建相依模組 (作者已經打包在模組檔中,無需單獨下載)
mods.dependency.incompatible=不相容模組 (同時安裝此模組和正在下載的模組會導致遊戲無法啟動)
mods.dependency.broken=損壞的相依模組 (該模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源)
mods.dependency.broken=損壞的相依模組 (該模組曾經存在於模組儲存庫中,但現在已被刪除,請嘗試其他下載源)
mods.disable=停用
mods.download=模組下載
mods.download.title=模組下載 - %1s
@@ -1031,9 +1031,9 @@ profile.title=遊戲目錄
profile.selected=已選取
profile.use_relative_path=如可行,則對於遊戲目錄使用相對路徑
repositories.custom=自訂 Maven 倉庫 (%s)
repositories.custom=自訂 Maven 儲存庫 (%s)
repositories.maven_central=全球 (Maven Central)
repositories.tencentcloud_mirror=中國大陸 (騰訊雲 Maven 倉庫)
repositories.tencentcloud_mirror=中國大陸 (騰訊雲 Maven 儲存庫)
repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正常執行。\n點擊「確認」從指定下載源下載 JavaFX 執行時元件並開啟 HMCL。點擊「取消」退出程式。\n選取下載源:
repositories.chooser.title=選取 JavaFX 下載源
@@ -1152,9 +1152,13 @@ settings.advanced.post_exit_command=遊戲結束後執行指令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.renderer=繪製器
settings.advanced.renderer.default=預設
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (軟繪製器)
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.unsupported_system_options=不適用於目前系統的選項
@@ -1277,7 +1281,7 @@ terracotta.difficulty.easiest=目前網路狀況極佳:稍等一下就成功
terracotta.difficulty.simple=目前網路狀況較好:建立連線需要一段時間…
terracotta.difficulty.medium=目前網路狀態中等:已啟用抗干擾備用線路,連線可能失敗
terracotta.difficulty.tough=目前網路狀態極差:已啟用抗干擾備用線路,連線可能失敗
terracotta.difficulty.estimate_only=連接成功率由房主和房客的 NAT 類型推算得到,僅供參考。
terracotta.difficulty.estimate_only=連線成功率由房主和房客的 NAT 類型推算得到,僅供參考。
terracotta.from_local.title=線上核心第三方下載管道
terracotta.from_local.desc=在部分地區,內建的預設下載管道可能不穩定或連線緩慢
terracotta.from_local.guide=您應下載名為 %s 的線上核心套件。下載完成後,請將檔案拖曳到目前介面來安裝。
@@ -1336,7 +1340,7 @@ terracotta.status.fatal.network=未能下載線上核心。請檢查網路連接
terracotta.status.fatal.install=嚴重錯誤:無法安裝線上核心
terracotta.status.fatal.terracotta=嚴重錯誤:無法與線上核心通訊
terracotta.status.fatal.unknown=嚴重錯誤:原因未知
terracotta.player_list=玩家列表
terracotta.player_list=玩家清單
terracotta.player_anonymous=匿名玩家
terracotta.player_kind.host=房主
terracotta.player_kind.local=你
@@ -1345,7 +1349,7 @@ terracotta.unsupported=多人聯機功能尚未支援目前平台。
terracotta.unsupported.os.windows.old=多人聯機功能需要 Windows 10 或更高版本。請更新系統。
terracotta.unsupported.arch.32bit=多人聯機功能不支援 32 位元系統。請更新至 64 位元系統。
terracotta.unsupported.arch.loongarch64_ow=多人聯機功能不支援 Linux LoongArch64 舊世界發行版,請更新至新世界發行版 (如 AOSC OC)。
terracotta.unsupported.region=多人聯機功能目前僅為中國內地用戶提供服務,在您所在地區可能不可用。
terracotta.unsupported.region=多人聯機功能目前僅為中國內地使用者提供服務,在您所在地區可能不可用。
unofficial.hint=你正在使用第三方提供的 HMCL。我們無法保證其安全性,請注意甄別。
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,7 @@
about=关于
about.copyright=版权
about.copyright.statement=版权所有 © 2026 huangyuhui
about.copyright.statement=版权所有 © 2013-2026 huangyuhui 及贡献者
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=用户协议
@@ -118,13 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软
account.methods.microsoft.logging_in=登录中……
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
account.methods.microsoft.manual=请在弹出的网页中输入上方显示的代码以完成登录。\n\
若网站未能显示,请手动在浏览器中打开:%s\n\
<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
account.methods.microsoft.methods.device=扫描二维码登录
account.methods.microsoft.methods.device.hint=扫描二维码或访问 <a href="%s">%s</a>,在打开的页面中输入 <b>%s</b> 完成登录。
account.methods.microsoft.methods.browser=在浏览器登录
account.methods.microsoft.methods.browser.hint=点击“登录”按钮或者<a href="%s">复制链接</a>并在浏览器中粘贴以登录。
account.methods.microsoft.manual=<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.profile=编辑账户个人信息
account.methods.microsoft.purchase=购买 Minecraft
account.methods.forgot_password=忘记密码
account.methods.ban_query=检测账户是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载 <a href="https://hmcl.huangyuhui.net/download">官方版本</a> 来登录微软账户。
account.methods.microsoft.snapshot.tooltip=你正在使用第三方提供的 HMCL。请下载官方版本来刷新账户。
@@ -364,7 +365,6 @@ download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mirror=尽量使用镜像源
download.provider.mirror.desc=加载快,但可能不是最新
download.java=下载 Java
download.java.override=此 Java 版本已经存在,是否卸载并重新安装?
download.java.process=下载 Java
download.javafx=正在下载必要的运行时组件……
download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源。点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源。
@@ -396,7 +396,6 @@ extension.schematic=原理图文件
extension.world=世界压缩包
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHello Minecraft! Launcher 无法在低于 Java 11 的 Java 环境上自行补全 JavaFX 运行环境。请更新到 Java 11 或更高版本。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incomplete=JavaFX 运行环境不完整。请尝试更换你的 Java 或者重新安装 OpenJFX。
fatal.javafx.missing=缺少 JavaFX 运行环境。请使用包含 OpenJFX 的 Java 运行环境启动 Hello Minecraft! Launcher。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft! Launcher,这可能导致你未来无法正常使用其他账户正常启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
@@ -414,7 +413,7 @@ fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe (软渲染器)”,以获得更好的兼容性。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -438,14 +437,14 @@ game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如
game.crash.info=游戏信息
game.crash.reason=崩溃原因
game.crash.reason.analyzing=分析中……
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
game.crash.reason.multiple=检测到多个原因:\n\n
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
@@ -697,6 +696,7 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=确认
modpack=整合包
modpack.choose=选择要安装的游戏整合包文件
@@ -1157,9 +1157,13 @@ settings.advanced.post_exit_command=游戏结束后执行命令
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
settings.advanced.renderer=渲染器
settings.advanced.renderer.default=默认
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (软渲染器)
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
settings.advanced.server_ip=服务器地址
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
settings.advanced.unsupported_system_options=不适用于当前系统的选项
@@ -19,20 +19,135 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<html lang="%lang%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Minecraft! Launcher</title>
<style>
%style%
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--monet-surface);
color: var(--monet-on-surface);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
user-select: none;
cursor: default;
}
.dialog-card {
background-color: var(--monet-surface-container-high);
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
max-width: 560px;
width: 90%;
display: flex;
flex-direction: row;
gap: 16px;
}
.icon-column {
flex-shrink: 0;
padding-top: 4px;
}
.icon-column svg {
width: 40px;
height: 40px;
display: block;
fill: var(--monet-on-surface);
}
.content-column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.title {
font-size: 20px;
line-height: 1.4;
font-weight: 500;
color: var(--monet-on-surface);
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
line-height: 1.5;
color: var(--monet-on-surface-variant);
word-wrap: break-word;
margin-bottom: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-confirm {
background-color: transparent;
color: var(--monet-primary);
border: none;
padding: 0 16px;
height: 40px;
font-size: 14px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s;
outline: none;
min-width: 64px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-confirm:hover {
background-color: rgba(128, 128, 128, 0.08);
}
.btn-confirm:active {
background-color: rgba(128, 128, 128, 0.12);
}
</style>
</head>
<body>
<div>
%close-page%
<div class="dialog-card">
<div class="icon-column">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/>
</svg>
</div>
<script>
setTimeout(function() {
open("about:blank","_self").close();
}, 5000);
</script>
</body>
<div class="content-column">
<div class="title">%success%</div>
<div class="subtitle">%close_page%</div>
<div class="actions">
<button class="btn-confirm" onclick="closeWindow()">%ok%</button>
</div>
</div>
</div>
<script>
function closeWindow() {
window.close();
window.open("about:blank", "_self").close();
}
setTimeout(closeWindow, 3000);
</script>
</body>
</html>
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more