Compare commits

...
88 Commits
Author SHA1 Message Date
庐庐绫 d297417909 fix: show/hide 前先 setIconified(false) 确保窗口状态正常 (#6525)
close #6523
2026-07-28 09:40:06 +08:00
Glavo 2854eac27d 在高刷屏上自动提高动画帧率 (#6422) 2026-07-27 21:30:27 +08:00
ToobLac c9fd7c6a1c [Enhancement] 优化主题包导出对话框的交互 (#6516)
Resolves #6512
2026-07-27 21:03:54 +08:00
Glavo 0e0517ab3c 优化模组信息读取速度 (#6515) 2026-07-27 07:12:00 +08:00
ToobLacandGlavo 7a4bea10d0 [Bugfix] 修复 #6348 资源包管理处理非UTF-8编码的options.txt文件时出错 (#6351)
Fixes #6348

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-07-26 21:31:35 +08:00
辞庐 b87d40ffcc 优化 实例列表无版本时点击下载页展示 (#6494) 2026-07-26 20:56:41 +08:00
Glavo 1277bf8b5d 使用 kala-encoding-detector 作为编码推测库 (#6477) 2026-07-26 00:47:43 +08:00
辞庐 c2e807055a fix: 下载整合包时未设置任务名 (#6487) 2026-07-25 21:07:43 +08:00
辞庐 9021ec1960 清理模组管理页面冗余 “实例无模组加载器” 提示 (#6500) 2026-07-25 21:01:22 +08:00
辞庐 82a47e9e2d 优化实例列表界面 (#6493) 2026-07-25 21:00:42 +08:00
neveler 4841204fa0 update i18n (#6491) 2026-07-25 21:00:26 +08:00
辞庐 897f5fe11b fix: 形参化类 'Tab<?>' 的原始使用 (#6498) 2026-07-25 20:57:58 +08:00
Glavo 4657e3273e 复制实例时同步保存游戏设置 (#6492) 2026-07-24 20:39:59 +08:00
Glavo 4edea5f5b9 Add support for Minecraft 26.3-snapshot-5+ to unofficial supported platforms (#6481) 2026-07-23 19:51:00 +08:00
辞庐 36bd7d4491 修复 GameAdvancedListItem#loadVersion UI 操作可能不会 JavaFX 线程运行的问题 (#6445)
resolves #6443
2026-07-22 22:11:55 +08:00
neveler 3aa8603bd1 提取 pr-size-label 工作流中的 js 代码到独立文件 (#6458) 2026-07-22 22:06:57 +08:00
庐庐绫 ca7a61e20a fix: 修复模组列表加载时检查错误实例的模组更新 (#6478)
close #6448
2026-07-22 21:57:59 +08:00
辞庐 21afc81cf8 fix: NetwordUtils#doPost(URI u, Map<String, String> params) 未进行 URL 编码 (#6457) 2026-07-22 21:49:46 +08:00
neveler 94a0e26be5 update i18n: remove game.crash.reason.mod_repeat_installation (#6472) 2026-07-22 21:47:13 +08:00
辞庐 14e8d2d0b5 清理 VanillaInstallWizardProvider (#6474) 2026-07-22 21:46:49 +08:00
快乐凤爪王 49c408cb23 Refactor: InstallersPageSkin versionNamePane (#6349) 2026-07-22 21:45:49 +08:00
辞庐 b81c3398ac fix: 切换版本隔离设置后自动刷新其他子页面 功能失效 (#6468) 2026-07-22 13:21:58 +08:00
快乐凤爪王 2855df3693 Feat: Implement dynamic folder icon and use to game directory list (#6435) 2026-07-21 22:03:32 +08:00
快乐凤爪王 8184dc6cee Fix: Alignment for background opacity percentage label (#6450) 2026-07-21 21:55:12 +08:00
快乐凤爪王 df21559c0e Chore: Clean up duplicate key-value pairs for No results found (#6449) 2026-07-21 21:53:21 +08:00
快乐凤爪王 73ff3081f8 Feat: No results found tip for ThemePackManagementPage (#6451) 2026-07-21 21:49:52 +08:00
辞庐 fe05bb58a4 更新时 日志输出新进程命令行时 打码敏感信息 (#6456) 2026-07-21 21:30:32 +08:00
Glavo 4c9cd950e4 移除对 JavaFX 14 的兼容性 (#6467) 2026-07-21 20:40:56 +08:00
neveler 646d9a09a5 update i18n (#6466) 2026-07-21 20:38:56 +08:00
Glavo 255fe75640 Bump JavaFX to 21.0.11 on FreeBSD x86-64 (#6463) 2026-07-21 19:40:18 +08:00
neveler de15dd8e98 Remove extra whitespace (#6462) 2026-07-21 17:57:57 +08:00
Glavo 886319a6c7 避免在代码中直接引用 Controllers.getStage() (#6431) 2026-07-20 21:41:55 +08:00
辞庐 3320f6c341 fix: 整合包安装完成回调执行器不统一 (#6446) 2026-07-20 21:21:33 +08:00
快乐凤爪王 b85be78166 Feat: Add empty state placeholders for versions (#6426) 2026-07-20 21:21:06 +08:00
Glavo a34a7439d7 Clarify nullability and documentation in AGENTS.md (#6440) 2026-07-20 00:02:59 +08:00
ZZZank 591fd51600 实现游戏日志窗口开关自动换行 (#6424) 2026-07-19 21:53:57 +08:00
快乐凤爪王 c1407a9620 Fix: Resolve ModLoaderType.LEGACY_FABRIC for Modrinth (#6434) 2026-07-19 02:46:28 +08:00
Glavo e0a91579f2 更新内置主题相关译文 (#6429) 2026-07-17 21:29:51 +08:00
快乐凤爪王 b68f404c3c Refactor: Style of ModInfoDialog (#6423) 2026-07-17 21:28:02 +08:00
Glavo c1f50f9ba3 删除 Gemini 审查配置文件 (#6430) 2026-07-17 21:17:31 +08:00
辞庐 d8bb2131f2 允许拖入 nbt 文件 (#6427) 2026-07-17 21:08:45 +08:00
Glavo aca761385b Add support for Minecraft 26.3-snapshot-4+ to unofficial supported platforms (#6425) 2026-07-17 21:00:14 +08:00
3gf8jv4dv 15faccf376 chore(workflow): add pr-size-label workflow file (#6381) 2026-07-17 20:23:48 +08:00
Glavo 241c9c419c 修复 FXUtils 意外的被提前初始化的问题 (#6421) 2026-07-16 21:29:12 +08:00
辞庐 4b36f8ce03 fix: 任务窗口速度与取消未对齐 (#6140) 2026-07-16 21:28:31 +08:00
Glavo 5c0baad782 提早加载配置文件 (#6420) 2026-07-16 20:50:20 +08:00
Apostrophe 850a56405c 修复游戏 Java 路径显示滞后问题 (#6419) 2026-07-16 19:59:17 +08:00
快乐凤爪王 e8487c467b Refactor: Replace deprecated URL constructor (#6409) 2026-07-16 03:37:54 +08:00
Glavo 9866d88a16 完善游戏设置页面帮助文本 (#6415) 2026-07-15 21:15:00 +08:00
辞庐 af2d46ae89 Revert "Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin" (#6410)
Reverts HMCL-dev/HMCL#6370
2026-07-15 19:56:20 +08:00
ToobLac 6f7c561ecb [Bugfix] 修复 1.21.1 安装信雅互联仍显示不支持 Fabric 模组的问题 (#6404)
Fixes #6391
2026-07-14 21:41:39 +08:00
ToobLac 93abe31acf [Chores] 处理升级到 9.6.1 后 Gradle 弃用提示 (#6405) 2026-07-14 21:29:58 +08:00
Glavo a6f8659349 Bump Gradle Wrapper Neo to 0.2.0 (#6397) 2026-07-14 00:06:56 +08:00
Glavo 8728c89093 Bump version root to 3.17 (#6392) 2026-07-13 20:34:38 +08:00
辞庐 2dccd8099d Bump authlib-injector to 1.2.8 (#6390) 2026-07-13 20:33:23 +08:00
Glavo d8a03cfed8 支持主题色跟随系统 (#6385) 2026-07-13 03:42:50 +08:00
Glavo 26a874a030 Bump Gradle to 9.6.1 (#6388) 2026-07-13 03:00:15 +08:00
Glavo 7c5c3e2fb4 使用 Gradle Wrapper Neo 代替 Gradle Wrapper (#6387) 2026-07-13 02:52:47 +08:00
Glavo 62d7b36248 添加“窗口透明”选项 (#6383) 2026-07-13 00:41:12 +08:00
Glavo dec6837528 修复 Minecraft 26.1+ 无法安装 Quilt 的问题 (#6386) 2026-07-13 00:38:52 +08:00
快乐凤爪王 3484792e81 Feat: Added status label opacity when installer item is incompatible (#6335) 2026-07-12 22:37:57 +08:00
快乐凤爪王 2b310e035d Fix: Lock AccountListItemSkin.btnRefresh width to prevent layout shifting (#6369) 2026-07-12 22:01:34 +08:00
Glavo 6e960612dd 固定默认游戏文件夹和官方启动器游戏文件夹的 ID (#6382) 2026-07-12 22:00:30 +08:00
辞庐 73a776b021 fix: 整合包安装界面高度限制失效 (#6371) 2026-07-12 21:46:46 +08:00
辞庐and3gf8jv4dv 31a2f49ed2 更新 fatal.unsupported_platform 设置指引 (#6251)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-07-12 20:49:51 +08:00
快乐凤爪王 ab268cfe48 Feat: DisplayName for JavaPackageType (#6377) 2026-07-12 20:35:32 +08:00
Glavo 721aeccfc0 修复 JFXPasswordField 导致 CPU 占用率异常增高的问题 (#6373) 2026-07-12 01:35:01 +08:00
Glavo e4348f727a 优化下载线程数设置 UI (#6374) 2026-07-12 01:34:17 +08:00
快乐凤爪王 dfc96f10da Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin (#6370) 2026-07-11 21:19:37 +08:00
快乐凤爪王 a5c7fe08d8 Feat: No results found tip for ModDownloadListPage (#6368) 2026-07-11 02:37:09 +08:00
Glavo a6e0acca45 在滚动条处于按下状态时保持不透明状态 (#6366) 2026-07-10 23:57:20 +08:00
Glavo 3d312f9247 修复在 JFXComboBox 弹出窗口的滚动条上释放鼠标时也会导致弹出窗口隐藏的问题 (#6365) 2026-07-10 21:44:51 +08:00
Glavo 18dc5b69c0 Fix #6320: 修复安装新实例时,Fabric API 安装路径未遵循版本隔离策略的问题 (#6355) 2026-07-10 20:36:29 +08:00
快乐凤爪王 98af2c85b6 Feat: Animate menu button arrow counter-clockwise on popup display (#6344) 2026-07-10 20:35:39 +08:00
Glavo c4c0b1a1ce 修复自动分配内存逻辑异常的问题 (#6356) 2026-07-10 20:21:44 +08:00
辞庐 5e621ce340 HttpRequest 添加 PUT 与 DELETE 请求支持 (#6312) 2026-07-09 21:02:14 +08:00
快乐凤爪王 1d1c8dfdc9 Fix: Adjusted the size of the ModInfoListCell ResourcePackListCell icon (#6347) 2026-07-09 20:55:33 +08:00
快乐凤爪王 9b68b8825d Chore: Removed unnecessary punctuation marks of allow_auto_agent.subtitle (#6346) 2026-07-09 20:55:11 +08:00
快乐凤爪王 5c17783223 Fix: Restore subtitle when RemoteVersionListCell is reused (#6329) 2026-07-08 22:03:59 +08:00
Glavo a921be45f1 Fix #6290: GameAdvancedListItem 应等待 HMCLGameRepository 加载完成后再加载图标 (#6325) 2026-07-08 21:59:06 +08:00
辞庐 2dd1c0f85e 优化日志窗口配色 (#6313) 2026-07-08 21:54:24 +08:00
快乐凤爪王 7ba229b3e5 Fix: Adjust GameListCell spacing (#6337) 2026-07-08 21:47:49 +08:00
辞庐 59a299630b fix: #6328 MCIM 描述没有对齐 (#6333) 2026-07-08 21:46:26 +08:00
快乐凤爪王 b41b89e233 Feat: Added numbered square tag to Java runtime list cells (#6334) 2026-07-08 21:40:50 +08:00
ToobLac af137e5ff1 [Bugfix] 修复 #6338: macOS 导出整合包默认名称包含2个后缀名 (#6340)
Fixes #6338
2026-07-08 21:39:37 +08:00
Glavo 74ddbf5623 Streamline memory allocation options and improve preset management (#6330)
Fix #6307
2026-07-08 21:15:36 +08:00
快乐凤爪王 accef4a93f Fix: WorldListPage MDListCell tags (#6339) 2026-07-08 20:37:16 +08:00
快乐凤爪王 9c3a955fad Refactor: Launch pane button (#6341) 2026-07-08 20:27:35 +08:00
142 changed files with 8070 additions and 1478 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
code_review:
pull_request_opened:
code_review: false
+105
View File
@@ -0,0 +1,105 @@
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const ignoredFilePatterns = [
/(?:^|\/)\.cnb\/.+\.ya?ml$/i,
/(?:^|\/)\.gitee\/.+\.ya?ml$/i,
/(?:^|\/)\.github\/.+\.ya?ml$/i,
/(?:^|\/)\.idea\/.+\.xml$/i,
/(?:^|\/)\.editorconfig$/i,
/(?:^|\/)\.gitattributes$/i,
/(?:^|\/)\.gitignore$/i,
/(?:^|\/).+\.kts$/i,
/(?:^|\/).+\.md$/i,
/(?:^|\/)LICENSE(?:_.*)?$/i,
/(?:^|\/)gradlew(?:\.bat|\.ps1)?$/i,
/(?:^|\/)gradle\/wrapper\/gradle-wrapper\.properties$/i,
/(?:^|\/)lang\/.+\.properties$/i,
/(?:^|\/)l10n\/.+\.properties$/i,
/(?:^|\/)checkstyle\.xml$/i,
/(?:^|\/).+\.(?:png|jpe?g|gif|ico|svg|json|ttf|woff2?|webp|avif|jxl|heic|mp4|webm|mp3|ogg|wav|txt|toml)$/i
];
async function processPR(issueNumber) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: issueNumber, per_page: 100,
});
const countedFiles = [];
const ignoredFiles = [];
let totalChanges = 0;
for (const file of files) {
const ignored = ignoredFilePatterns.some(pattern => pattern.test(file.filename));
if (ignored) {
ignoredFiles.push(file.filename);
continue;
}
const fileChanges = file.changes ?? ((file.additions || 0) + (file.deletions || 0));
totalChanges += fileChanges;
countedFiles.push(`${file.filename} (${fileChanges})`);
}
let targetLabel;
if (totalChanges <= 9) targetLabel = "1+";
else if (totalChanges <= 39) targetLabel = "10+";
else if (totalChanges <= 99) targetLabel = "40+";
else if (totalChanges <= 499) targetLabel = "100+";
else if (totalChanges <= 999) targetLabel = "500+";
else if (totalChanges <= 1999) targetLabel = "1000+";
else if (totalChanges <= 4999) targetLabel = "2000+";
else targetLabel = "5000+";
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner, repo, issue_number: issueNumber, per_page: 100,
});
const sizeLabels = currentLabels.map(label => label.name).filter(name => /^\d+\+$/.test(name));
for (const name of sizeLabels) {
if (name !== targetLabel) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: issueNumber, name,
}).catch(() => { });
}
}
if (!currentLabels.some(label => label.name === targetLabel)) {
await github.rest.issues.addLabels({
owner, repo, issue_number: issueNumber, labels: [targetLabel],
});
}
core.info(`Pull request #${issueNumber} has ${totalChanges} counted changed lines, applied ${targetLabel}.`);
core.info(`Ignored files: ${ignoredFiles.length ? ignoredFiles.join(", ") : "none"}`);
core.info(`Counted files: ${countedFiles.length ? countedFiles.join(", ") : "none"}`);
}
if (context.eventName === "workflow_dispatch") {
core.info("Manual trigger detected. Fetching all open pull requests...");
const openPRs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: "open", per_page: 100,
});
const unlabelledPRs = openPRs.filter(pr => {
return !pr.labels.some(label => /^\d+\+$/.test(label.name));
});
core.info(`Found ${openPRs.length} open pull request(s), ${unlabelledPRs.length} of them lack a size label.`);
for (const pr of unlabelledPRs) {
core.info(`----------------------------------------`);
core.info(`Processing unlabelled pull request #${pr.number}: ${pr.title}`);
await processPR(pr.number);
}
} else {
const pr = context.payload.pull_request;
if (!pr) {
core.info("No pull request found in payload, skipping.");
return;
}
await processPR(pr.number);
}
}
+31
View File
@@ -0,0 +1,31 @@
name: PR Size Label
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts
sparse-checkout-cone-mode: true
- name: Label PR by filtered changed lines
uses: actions/github-script@v9
with:
script: |
const script = require("./.github/scripts/pr-size-label.js");
await script({ github, context, core });
+5 -3
View File
@@ -4,9 +4,9 @@ These rules apply to all Java code written or modified in this repository.
## Nullability
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability must never be implicit.
- Every class declared in a newly added Java source file must be annotated with JetBrains Annotations `@NotNullByDefault`.
- When writing or modifying Java code, any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability in code being written or modified must never be implicit.
## Immutability
@@ -15,6 +15,8 @@ These rules apply to all Java code written or modified in this repository.
## Documentation
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
- Every class, field, and method must have documentation.
- Documentation must use `///` Markdown-style Javadoc comments.
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
+7 -7
View File
@@ -52,7 +52,7 @@ if (buildNumber != null) {
}
}
val embedResources by configurations.registering
val embedResources = configurations.register("embedResources")
dependencies {
implementation(project(":HMCLCore"))
@@ -166,7 +166,7 @@ val hmclProperties = buildList {
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile by tasks.registering {
val createPropertiesFile = tasks.register("createPropertiesFile") {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
@@ -258,7 +258,7 @@ tasks.processResources {
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
val makeExecutables by tasks.registering {
val makeExecutables = tasks.register("makeExecutables") {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
@@ -286,7 +286,7 @@ val makeExecutables by tasks.registering {
}
}
val makeDeb by tasks.registering(CreateDeb::class) {
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
dependsOn(makeExecutables)
val debFile = layout.file(provider { artifactFile("deb") })
@@ -438,19 +438,19 @@ tasks.register<CheckTranslations>("checkTranslations") {
val generatedDir = layout.buildDirectory.dir("generated")
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
}
val createLanguageList by tasks.registering(CreateLanguageList::class) {
val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList") {
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
resourceBundleBaseName.set("I18N")
additionalLanguages.set(listOf("en-Qabs"))
outputFile.set(generatedDir.map { it.file("languages.json") })
}
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
dependsOn(createLanguageList)
languagesFile.set(createLanguageList.flatMap { it.outputFile })
@@ -38,7 +38,6 @@ import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
@@ -68,14 +67,18 @@ public class JFXPasswordField extends PasswordField {
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
}
useJFXContextMenu(this);
}
/// Prevents the legacy JFoenix skin from continuously requesting another layout pass.
// https://github.com/HMCL-dev/HMCL/issues/5822
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
@Override
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
/**
* Initialize the style class to 'jfx-password-field'.
* <p>
@@ -294,4 +297,3 @@ public class JFXPasswordField extends PasswordField {
}
}
@@ -15,9 +15,13 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@@ -25,6 +29,8 @@ import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import java.util.List;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
@@ -150,6 +156,30 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
}
});
for (Node child : getChildren()) {
if (child instanceof ListView<?> listView && "list-view".equals(listView.getId())) {
// The default hideOnClick logic of ComboBoxListViewSkin doesn't work well with FloatScrollBarSkin.
// We implement hideOnClick ourselves to prevent the popup from
// hiding when releasing the mouse on the scrollbar.
this.setHideOnClick(false);
listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
EventTarget target = t.getTarget();
if (target instanceof Node targetNode) {
List<String> s = targetNode.getStyleClass();
if (s.contains("thumb")
|| s.contains("track")
|| s.contains("decrement-arrow")
|| s.contains("increment-arrow")) {
return;
}
}
comboBox.hide();
});
}
}
}
protected void layoutChildren(double x, double y, double w, double h) {
@@ -52,8 +52,6 @@ public final class EntryPoint {
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
if (!isInsideMacAppBundle())
@@ -74,71 +72,6 @@ public final class EntryPoint {
System.exit(exitCode);
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_LOCAL_HOME)) {
try {
@@ -225,15 +158,8 @@ public final class EntryPoint {
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
showWarning(i18n("fatal.wine_warning"));
}
}
@@ -273,10 +199,21 @@ public final class EntryPoint {
}
}
static void showWarning(String message) {
SwingUtils.initLookAndFeel();
int result = JOptionPane.showOptionDialog(null, message, i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
static void showErrorAndExit(String message) {
SwingUtils.showErrorDialog(message);
exit(1);
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl;
import com.sun.jna.Pointer;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
@@ -29,8 +30,6 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
@@ -48,7 +47,13 @@ import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
import org.jackhuang.hmcl.util.platform.windows.User32;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
@@ -60,7 +65,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -98,18 +102,7 @@ public final class Launcher extends Application {
}
try {
try {
SettingsManager.init();
initializeSettingsRuntime();
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
EntryPoint.exit(1);
}
initializeSettingsRuntime();
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
@@ -274,52 +267,10 @@ public final class Launcher extends Application {
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
Path configDirectory = SettingsManager.localConfigDirectory();
if (!Files.exists(configDirectory)) {
return;
}
String owner;
try {
owner = Files.getOwner(configDirectory).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(configDirectory.toString());
if (Files.exists(Metadata.HMCL_USER_HOME))
files.add(Metadata.HMCL_USER_HOME.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
i18n("fatal.config_loading_failure.unix", owner, command),
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
EntryPoint.exit(1);
}
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
FileSaver.shutdown();
SettingsManager.shutdown();
LOG.shutdown();
}
@@ -376,6 +327,19 @@ public final class Launcher extends Application {
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
try {
SettingsManager.init();
} catch (SambaException e) {
EntryPoint.showWarning(i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigOwner();
SwingUtils.showErrorDialog(i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
EntryPoint.exit(1);
}
setupJavaFXVMOptions();
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
@@ -408,5 +372,158 @@ public final class Launcher extends Application {
});
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
setUpAnimationFrameRate:
{
if (System.getProperty("javafx.animation.pulse") != null) {
break setUpAnimationFrameRate;
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
int value = Integer.parseInt(animationFrameRate);
if (value <= 0)
throw new NumberFormatException(animationFrameRate);
if (value != 60)
System.setProperty("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
break setUpAnimationFrameRate;
}
// To avoid prematurely loading FXUtils, we only check if animationDisabled has been explicitly set to true
if (Boolean.TRUE.equals(settings().animationDisabledProperty().get()))
break setUpAnimationFrameRate;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (NativeUtils.USE_JNA && Gdi32.INSTANCE != null && User32.INSTANCE != null) {
@Nullable Pointer pointer = User32.INSTANCE.GetDC(Pointer.NULL);
if (pointer != null) {
try {
int refreshRate = Gdi32.INSTANCE.GetDeviceCaps(pointer, Gdi32.VREFRESH);
if (refreshRate > 0) {
LOG.info("Detected refresh rate: " + refreshRate + "Hz");
if (refreshRate >= 90) {
System.getProperties().putIfAbsent("javafx.animation.pulse", String.valueOf(refreshRate));
}
}
} finally {
User32.INSTANCE.ReleaseDC(Pointer.NULL, pointer);
}
}
}
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
Path configDirectory = SettingsManager.localConfigDirectory();
if (!Files.exists(configDirectory)) {
return;
}
String owner;
try {
owner = Files.getOwner(configDirectory).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(configDirectory.toString());
if (Files.exists(Metadata.HMCL_USER_HOME))
files.add(Metadata.HMCL_USER_HOME.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
SwingUtils.initLookAndFeel();
Object[] options = {i18n("button.copy_and_exit"), i18n("button.cancel")};
int result = JOptionPane.showOptionDialog(null,
i18n("fatal.config_loading_failure.unix", owner, command),
i18n("message.error"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0]);
if (result == 0) {
try {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(command), null);
} catch (Throwable e) {
LOG.warning("Failed to copy command to clipboard", e);
}
}
EntryPoint.exit(1);
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
@@ -242,6 +242,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id));
}
/// Removes a version from disk and drops any cached instance settings for that version.
@Override
public boolean removeVersionFromDisk(String id) {
boolean removed = super.removeVersionFromDisk(id);
if (removed) {
instanceGameSettings.remove(id);
loadedInstanceGameSettings.remove(id);
readOnlyInstanceGameSettings.remove(id);
beingModpackVersions.remove(id);
}
return removed;
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
@@ -284,7 +297,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
newGameSettings.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
newGameSettings.runningDirectoryProperty().setValue("");
initInstanceGameSettings(dstId, newGameSettings);
saveGameSettings(dstId);
saveGameSettingsSync(dstId);
Path dstGameDir = getRunDirectory(dstId);
@@ -382,8 +395,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
+ file + ", Actual: " + schemaResult.actual());
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA ->
LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
}
}
@@ -520,31 +534,35 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
public boolean shouldIsolateNewInstance(boolean modded) {
GameSettings.Preset preset = getParentGameSettings(null);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
return switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> modded;
};
}
public void applyDefaultIsolationSetting(String id) {
if (!hasVersion(id)) {
/// Applies default isolation to a new instance before the version metadata is saved.
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
return;
}
GameSettings.Instance instanceSetting = getInstanceGameSettings(id);
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
boolean isolated = switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
};
if (isolated) {
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
if (setting != null) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
GameSettings.Instance setting = getInstanceGameSettings(id);
if (setting == null) {
setting = initInstanceGameSettings(id, new GameSettings.Instance());
}
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
saveGameSettings(id);
}
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
}
public Optional<Path> getVersionIconFile(String id) {
@@ -684,7 +702,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
/// Result of loading an instance-specific game settings file.
///
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param allowSave whether the file may be overwritten
private record InstanceGameSettingsLoadResult(
@Nullable GameSettings.Instance setting,
@@ -694,26 +712,31 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
GameSettings.Effective vs = getEffectiveGameSettings(version);
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
boolean autoMemory = vs.get(GameSettings::autoMemoryProperty);
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
@Nullable Integer maxMemory;
if (autoMemory) {
maxMemory = noJVMOptions
? null
: Math.toIntExact(getAutoAllocatedMemory(SystemInfo.getPhysicalMemoryStatus().available()) / 1024L / 1024L);
} else {
maxMemory = vs.getMaxMemory();
}
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.get(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty)))
.setMaxMemory(noJVMOptions && autoMemory ? null : (int) (getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
SystemInfo.getPhysicalMemoryStatus().available(),
autoMemory
) / 1024 / 1024))
.setMinMemory(vs.get(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.get(GameSettings::permSizeProperty)))
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
.setMaxMemory(maxMemory)
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.get(GameSettings::environmentVariablesProperty))
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
.stream()
.map(it -> {
int idx = it.indexOf('=');
@@ -731,16 +754,16 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
.setNoGeneratedJVMArgs(noJVMOptions)
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
.setUseCustomNatives(vs.get(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.get(GameSettings::nativesDirectoryProperty))
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
.setRenderer(vs.getRenderer(gameVersionNumber))
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
.setUseNativeGLFW(vs.get(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.get(GameSettings::useNativeOpenALProperty))
.setUseNativeGLFW(vs.getInheritable(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
@@ -838,22 +861,21 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
public static long getAutoAllocatedMemory(long available) {
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (usable <= 0) {
return available;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested;
if (usable <= threshold)
suggested = (long) (usable * 0.8);
else
suggested = Math.min(
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return suggested;
}
public static ProxyOption getProxyOption() {
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -544,7 +545,7 @@ public final class LauncherHelper {
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (!setting.get(GameSettings::useCustomNativesProperty)) {
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
@@ -581,7 +582,7 @@ public final class LauncherHelper {
}
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
if (java.getBits() == Bits.BIT_32 && !setting.getInheritable(GameSettings::autoMemoryProperty) && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
@@ -628,7 +629,7 @@ public final class LauncherHelper {
suggestions.add(i18n("launch.advice.modlauncher8"));
break;
case VANILLA_X86:
if (!setting.get(GameSettings::useCustomNativesProperty)
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)
&& Platform.isSupportedTranslationX86_64()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
@@ -640,7 +641,7 @@ public final class LauncherHelper {
// Cannot allocate too much memory exceeding free space.
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
if (totalMemorySizeMB > 0 && !setting.getInheritable(GameSettings::autoMemoryProperty) && totalMemorySizeMB < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
}
@@ -760,7 +761,10 @@ public final class LauncherHelper {
case HIDE_AND_REOPEN:
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
.ifPresent(stage -> {
stage.setIconified(false);
stage.show();
});
});
break;
case KEEP:
@@ -895,8 +899,10 @@ public final class LauncherHelper {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
@Nullable Stage stage = Controllers.getStage();
if (stage != null) {
stage.setIconified(false);
stage.hide();
launchingLatch.countDown();
}
});
@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl.game;
import kala.encdet.EncodingDetector;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -91,7 +91,7 @@ public final class LogExporter {
for (Path file : stream) {
if (Files.isRegularFile(file)) {
if (logMatcher == null || logMatcher.matches(file)) {
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
try (BufferedReader reader = EncodingDetector.MODERN_WEB.newBufferedReader(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
@@ -231,7 +231,7 @@ public final class ModpackHelper {
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
@@ -358,10 +358,10 @@ public final class ModpackHelper {
GameSettings.PROPERTY_MAX_MEMORY,
GameSettings.PROPERTY_PERM_SIZE
));
setting.autoMemoryProperty().setValue(effective.get(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.get(GameSettings::minMemoryProperty));
setting.autoMemoryProperty().setValue(effective.getInheritable(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.getInheritable(GameSettings::minMemoryProperty));
setting.maxMemoryProperty().setValue(manifest.getLaunchInfo().getMinMemory());
setting.permSizeProperty().setValue(effective.get(GameSettings::permSizeProperty));
setting.permSizeProperty().setValue(effective.getInheritable(GameSettings::permSizeProperty));
}
});
}
@@ -34,8 +34,6 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Objects;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
/// Persistent configuration for a game directory.
@JsonAdapter(GameDirectory.Serializer.class)
@NotNullByDefault
@@ -118,7 +116,7 @@ public final class GameDirectory implements Observable {
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
this.legacyGameSettings = new SimpleObjectProperty<>(this, "legacyGameSettings", legacyGameSettings);
addPropertyChangedListener(onInvalidating(this::invalidate));
addPropertyChangedListener(arg -> invalidate());
}
/// Returns a debug string containing the game directory path and display metadata.
@@ -39,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
@@ -54,6 +55,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@NotNullByDefault
public final class GameDirectoryManager {
/// The stable ID shared by newly created and migrated current-workspace game directories.
static final GameDirectoryID DEFAULT_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0x7105bc1f490e5e8cL, 0x878cf5844c3d4bc3L));
/// The stable ID shared by newly created and migrated user-home game directories.
static final GameDirectoryID HOME_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0xf3eafde8506e5a77L, 0xbc88f24b4728dfb2L));
/// The default current-workspace game directory path.
private static final PortablePath CURRENT_GAME_DIRECTORY_PATH = PortablePath.of(".minecraft");
@@ -154,12 +163,14 @@ public final class GameDirectoryManager {
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = localGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
}
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
@@ -216,9 +227,11 @@ public final class GameDirectoryManager {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
userGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
return true;
} else {
return false;
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
public sealed abstract class GameSettings extends ObservableSetting {
/// Suggested maximum heap memory in MiB.
static final int SUGGESTED_MEMORY;
public static final int SUGGESTED_MEMORY;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
@@ -331,6 +331,11 @@ public sealed abstract class GameSettings extends ObservableSetting {
}
/// Creates a new inheritable property.
protected final <T extends @Nullable Object> InheritableProperty<@Nullable T> newInheritableProperty(String name) {
return new SimpleInheritableProperty<>(this, name, null);
}
/// Creates a new inheritable property with the given default value.
protected final <T extends @UnknownNullability Object> InheritableProperty<T> newInheritableProperty(String name, T defaultValue) {
return new SimpleInheritableProperty<>(this, name, defaultValue);
}
@@ -388,10 +393,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized JVM options.
@SerializedName(PROPERTY_JVM_OPTIONS)
private final SettingProperty<String> jvmOptions = newSettingProperty(PROPERTY_JVM_OPTIONS, "");
private final InheritableProperty<String> jvmOptions = newInheritableProperty(PROPERTY_JVM_OPTIONS, "");
/// Returns the user customized JVM options property.
public SettingProperty<String> jvmOptionsProperty() {
public InheritableProperty<String> jvmOptionsProperty() {
return jvmOptions;
}
@@ -448,10 +453,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will automatically adjust memory allocation.
@SerializedName(PROPERTY_AUTO_MEMORY)
private final SettingProperty<Boolean> autoMemory = newSettingProperty(PROPERTY_AUTO_MEMORY, true);
private final InheritableProperty<Boolean> autoMemory = newInheritableProperty(PROPERTY_AUTO_MEMORY, true);
/// Returns the automatic memory allocation property.
public SettingProperty<Boolean> autoMemoryProperty() {
public InheritableProperty<Boolean> autoMemoryProperty() {
return autoMemory;
}
@@ -460,10 +465,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The minimum heap memory in MiB.
@SerializedName(PROPERTY_MIN_MEMORY)
private final SettingProperty<@Nullable Integer> minMemory = newSettingProperty(PROPERTY_MIN_MEMORY);
private final InheritableProperty<@Nullable Integer> minMemory = newInheritableProperty(PROPERTY_MIN_MEMORY);
/// Returns the minimum heap memory property.
public SettingProperty<@Nullable Integer> minMemoryProperty() {
public InheritableProperty<@Nullable Integer> minMemoryProperty() {
return minMemory;
}
@@ -472,10 +477,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The maximum heap memory in MiB.
@SerializedName(PROPERTY_MAX_MEMORY)
private final SettingProperty<@Nullable Integer> maxMemory = newSettingProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
private final InheritableProperty<@Nullable Integer> maxMemory = newInheritableProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
/// Returns the maximum heap memory property.
public SettingProperty<@Nullable Integer> maxMemoryProperty() {
public InheritableProperty<@Nullable Integer> maxMemoryProperty() {
return maxMemory;
}
@@ -484,10 +489,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The permanent generation or metaspace size in MiB.
@SerializedName(PROPERTY_PERM_SIZE)
private final SettingProperty<String> permSize = newSettingProperty(PROPERTY_PERM_SIZE, "");
private final InheritableProperty<String> permSize = newInheritableProperty(PROPERTY_PERM_SIZE, "");
/// Returns the permanent generation or metaspace size property.
public SettingProperty<String> permSizeProperty() {
public InheritableProperty<String> permSizeProperty() {
return permSize;
}
@@ -593,10 +598,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized arguments passed to the game.
@SerializedName(PROPERTY_GAME_ARGS)
private final SettingProperty<String> gameArguments = newSettingProperty(PROPERTY_GAME_ARGS, "");
private final InheritableProperty<String> gameArguments = newInheritableProperty(PROPERTY_GAME_ARGS, "");
/// Returns the customized game arguments property.
public SettingProperty<String> gameArgumentsProperty() {
public InheritableProperty<String> gameArgumentsProperty() {
return gameArguments;
}
@@ -643,10 +648,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized environment variables passed to the game.
@SerializedName(PROPERTY_ENVIRONMENT_VARIABLES)
private final SettingProperty<String> environmentVariables = newSettingProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
private final InheritableProperty<String> environmentVariables = newInheritableProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
/// Returns the customized environment variables property.
public SettingProperty<String> environmentVariablesProperty() {
public InheritableProperty<String> environmentVariablesProperty() {
return environmentVariables;
}
@@ -763,10 +768,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL does not patch native libraries.
@SerializedName(PROPERTY_NOT_PATCH_NATIVES)
private final SettingProperty<Boolean> notPatchNatives = newSettingProperty(PROPERTY_NOT_PATCH_NATIVES, false);
private final InheritableProperty<Boolean> notPatchNatives = newInheritableProperty(PROPERTY_NOT_PATCH_NATIVES, false);
/// Returns the native library patching property.
public SettingProperty<Boolean> notPatchNativesProperty() {
public InheritableProperty<Boolean> notPatchNativesProperty() {
return notPatchNatives;
}
@@ -775,10 +780,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL leaves native library management to the user.
@SerializedName(PROPERTY_USE_CUSTOM_NATIVES)
private final SettingProperty<Boolean> useCustomNatives = newSettingProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
private final InheritableProperty<Boolean> useCustomNatives = newInheritableProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
/// Returns the custom native libraries property.
public SettingProperty<Boolean> useCustomNativesProperty() {
public InheritableProperty<Boolean> useCustomNativesProperty() {
return useCustomNatives;
}
@@ -787,10 +792,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The path to the native library directory, or an empty string for the default path.
@SerializedName(PROPERTY_NATIVES_DIRECTORY)
private final SettingProperty<String> nativesDirectory = newSettingProperty(PROPERTY_NATIVES_DIRECTORY, "");
private final InheritableProperty<String> nativesDirectory = newInheritableProperty(PROPERTY_NATIVES_DIRECTORY, "");
/// Returns the native library directory property.
public SettingProperty<String> nativesDirectoryProperty() {
public InheritableProperty<String> nativesDirectoryProperty() {
return nativesDirectory;
}
@@ -799,10 +804,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native GLFW.
@SerializedName(PROPERTY_USE_NATIVE_GLFW)
private final SettingProperty<Boolean> useNativeGLFW = newSettingProperty(PROPERTY_USE_NATIVE_GLFW, false);
private final InheritableProperty<Boolean> useNativeGLFW = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW, false);
/// Returns the native GLFW property.
public SettingProperty<Boolean> useNativeGLFWProperty() {
public InheritableProperty<Boolean> useNativeGLFWProperty() {
return useNativeGLFW;
}
@@ -811,10 +816,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native OpenAL.
@SerializedName(PROPERTY_USE_NATIVE_OPENAL)
private final SettingProperty<Boolean> useNativeOpenAL = newSettingProperty(PROPERTY_USE_NATIVE_OPENAL, false);
private final InheritableProperty<Boolean> useNativeOpenAL = newInheritableProperty(PROPERTY_USE_NATIVE_OPENAL, false);
/// Returns the native OpenAL property.
public SettingProperty<Boolean> useNativeOpenALProperty() {
public InheritableProperty<Boolean> useNativeOpenALProperty() {
return useNativeOpenAL;
}
@@ -898,11 +903,6 @@ public sealed abstract class GameSettings extends ObservableSetting {
return instance;
}
/// Returns the effective value for a property whose override state is tracked by the property itself.
public <T extends @UnknownNullability Object> T get(Function<GameSettings, SettingProperty<T>> propertyGetter) {
return inherited(preset, instance, propertyGetter);
}
/// Returns the effective value for an inheritable property.
public <T extends @UnknownNullability Object> T getInheritable(
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
@@ -981,7 +981,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// Returns the effective maximum heap memory in MiB.
public int getMaxMemory() {
Integer value = get(GameSettings::maxMemoryProperty);
Integer value = getInheritable(GameSettings::maxMemoryProperty);
return value != null && value > 0 ? value : SUGGESTED_MEMORY;
}
@@ -156,7 +156,14 @@ final class JsonSettingFile<T extends ObservableSetting & JsonSchemaSetting> {
///
/// @param value the settings object to observe
void installAutoSave(T value) {
value.addListener(source -> save(value));
value.addListener(observable -> {
if (value.shouldSaveImmediately(observable)) {
save(value);
value.setSavePending(false);
} else {
value.setSavePending(true);
}
});
}
/// Saves a settings object.
@@ -87,6 +87,9 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Theme appearance override key for title-bar transparency.
public static final String THEME_APPEARANCE_TITLE_BAR_TRANSPARENT = "titleBarTransparent";
/// Theme appearance override key for window transparency.
public static final String THEME_APPEARANCE_WINDOW_TRANSPARENT = "windowTransparent";
/// Theme appearance override key for the primary background source.
public static final String THEME_APPEARANCE_BACKGROUND = "background";
@@ -336,6 +339,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
return titleBarTransparent;
}
/// Whether translucent launcher backgrounds reveal content behind the window.
@SerializedName("windowTransparent")
private final BooleanProperty windowTransparent = new SimpleBooleanProperty(false);
/// Returns the window transparency property.
public BooleanProperty windowTransparentProperty() {
return windowTransparent;
}
// Background source
/// The launcher background source type.
@@ -468,17 +480,19 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Whether UI animations are disabled.
@SerializedName("animationDisabled")
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
FXUtils.REDUCED_MOTION == Boolean.TRUE
|| !JavaRuntime.CURRENT_JIT_ENABLED
|| !FXUtils.GPU_ACCELERATION_ENABLED
);
private final ObjectProperty<@Nullable Boolean> animationDisabled = new SimpleObjectProperty<>();
/// Returns the UI animation disable property.
public BooleanProperty animationDisabledProperty() {
public ObjectProperty<@Nullable Boolean> animationDisabledProperty() {
return animationDisabled;
}
public boolean isAnimationDisabled() {
// Avoid accessing FXUtils too early during startup
return Objects.requireNonNullElseGet(animationDisabled.get(), () ->
FXUtils.REDUCED_MOTION == Boolean.TRUE || !JavaRuntime.CURRENT_JIT_ENABLED || !FXUtils.GPU_ACCELERATION_ENABLED);
}
// Networks
/// Whether HMCL automatically selects the number of download threads.
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
@@ -54,6 +55,15 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
register();
}
/// Returns whether the changed field should trigger automatic persistence immediately.
@Override
public boolean shouldSaveImmediately(Observable observable) {
return observable != x
&& observable != y
&& observable != width
&& observable != height;
}
/// The schema used by this launcher state file.
@SerializedName(JsonSchema.PROPERTY_SCHEMA)
private final ObjectProperty<JsonSchema> schema = new SimpleObjectProperty<>(CURRENT_SCHEMA);
@@ -81,12 +81,6 @@ public final class LegacyConfigMigrator {
/// The legacy built-in profile name for the user-home game directory.
private static final String LEGACY_HOME_PROFILE = "Home";
/// The legacy built-in current-workspace profile ID.
private static final GameDirectoryID LEGACY_DEFAULT_PROFILE_ID = getLegacyProfileID(LEGACY_DEFAULT_PROFILE);
/// The legacy built-in user-home profile ID.
private static final GameDirectoryID LEGACY_HOME_PROFILE_ID = getLegacyProfileID(LEGACY_HOME_PROFILE);
/// The legacy Windows and portable configuration file name used before HMCL 3.16.
private static final String LEGACY_CONFIG_FILENAME = "hmcl.json";
@@ -261,6 +255,10 @@ public final class LegacyConfigMigrator {
renameMember(jsonObject, "bgpaint", "customBackgroundPaint");
renameMember(jsonObject, "backgroundPaint", "customBackgroundPaint");
migrateBackgroundOpacity(jsonObject, themeAppearanceOverrides);
jsonObject.addProperty("windowTransparent", true);
addThemeAppearanceOverride(
themeAppearanceOverrides,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT);
writeThemeAppearanceOverrides(jsonObject, themeAppearanceOverrides);
renameMember(jsonObject, "proxyUserName", "proxyUser");
migrateLegacySelectedVersions(jsonObject);
@@ -1359,10 +1357,10 @@ public final class LegacyConfigMigrator {
/// Returns the legacy profile name used in `configurations`.
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_DEFAULT_PROFILE;
}
if (LEGACY_HOME_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.HOME_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_HOME_PROFILE;
}
return GameDirectoryManager.getGameDirectoryCustomName(gameDirectory);
@@ -55,6 +55,20 @@ public final class SettingsManager {
private SettingsManager() {
}
/// Saves deferred launcher state changes and shutdown file saver.
public static void shutdown() {
savePendingChanges();
FileSaver.shutdown();
}
/// Saves deferred launcher state changes.
public static void savePendingChanges() {
if (launcherState != null && launcherState.isSavable() && launcherState.isSavePending()) {
launcherState.setSavePending(false);
STATE_FILE.save(launcherState);
}
}
/// The local directory storing per-workspace configuration files.
private static final Path LOCAL_CONFIG_FILES_DIRECTORY = Metadata.HMCL_LOCAL_HOME.resolve("config");
@@ -28,19 +28,12 @@ import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.ResolvedTheme;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
@@ -67,23 +60,8 @@ public final class StyleSheets {
});
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
else
try {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
Files.writeString(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
return fallback;
}
private static String toStyleSheetUri(String styleSheet) {
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
}
private static String getFontStyleSheet() {
@@ -133,7 +111,7 @@ public final class StyleSheets {
builder.append('}');
return toStyleSheetUri(builder.toString(), defaultCss);
return toStyleSheetUri(builder.toString());
}
private static String getBrightnessStyleSheet() {
@@ -184,7 +162,7 @@ public final class StyleSheets {
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
builder.append("}\n");
return toStyleSheetUri(builder.toString(), blueCss);
return toStyleSheetUri(builder.toString());
}
public static void init(Scene scene) {
@@ -25,6 +25,9 @@ public enum ThemeColorType {
/// Uses the built-in launcher default theme color.
DEFAULT,
/// Follows the accent color provided by the operating system.
SYSTEM,
/// Uses the custom launcher theme color selected by the user.
CUSTOM,
@@ -0,0 +1,39 @@
/*
* 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.theme;
import javafx.scene.layout.Background;
import org.jetbrains.annotations.NotNullByDefault;
import java.util.Objects;
/// A launcher background and the opacity applied by its rendering node.
///
/// @param background the JavaFX background rendered by the launcher
/// @param opacity the node opacity in the range `[0, 1]`
@NotNullByDefault
public record LauncherBackground(Background background, double opacity) {
/// Creates a launcher background value.
public LauncherBackground {
Objects.requireNonNull(background);
if (!Double.isFinite(opacity) || opacity < 0.0 || opacity > 1.0) {
throw new IllegalArgumentException("Launcher background opacity must be between 0 and 1: " + opacity);
}
}
}
@@ -43,6 +43,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
@NotNullByDefault
public record ThemeAppearance(
@Nullable ThemeColorSource color,
@@ -50,7 +51,8 @@ public record ThemeAppearance(
@Nullable ColorStyle colorStyle,
@Nullable Contrast contrast,
@Nullable ThemeBackgroundSettings background,
@Nullable ThemeTitleBar titleBar) {
@Nullable ThemeTitleBar titleBar,
@Nullable Boolean windowTransparent) {
/// JSON member name for the color seed.
static final String FIELD_COLOR = "color";
@@ -70,6 +72,9 @@ public record ThemeAppearance(
/// JSON member name for title-bar settings.
static final String FIELD_TITLE_BAR = "titleBar";
/// JSON member name for window transparency.
static final String FIELD_WINDOW_TRANSPARENT = "windowTransparent";
/// Creates an appearance patch.
///
/// @param color the color seed source, or `null` when inherited
@@ -78,6 +83,7 @@ public record ThemeAppearance(
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
public ThemeAppearance {
if (background != null && background.isEmpty()) {
background = null;
@@ -100,7 +106,8 @@ public record ThemeAppearance(
readColorStyle(object),
readContrast(object),
readBackground(object),
readTitleBar(object));
readTitleBar(object),
readWindowTransparent(object));
}
/// Returns whether this appearance contains no concrete fields.
@@ -112,7 +119,8 @@ public record ThemeAppearance(
&& colorStyle == null
&& contrast == null
&& background == null
&& titleBar == null;
&& titleBar == null
&& windowTransparent == null;
}
/// Adds this appearance patch's concrete fields to a JSON object.
@@ -139,6 +147,9 @@ public record ThemeAppearance(
if (titleBar != null) {
object.add(FIELD_TITLE_BAR, titleBar.toJsonObject());
}
if (windowTransparent != null) {
object.addProperty(FIELD_WINDOW_TRANSPARENT, windowTransparent);
}
}
/// Converts this appearance patch to its JSON representation.
@@ -167,7 +178,8 @@ public record ThemeAppearance(
: patch.background != null ? patch.background : background,
titleBar != null && patch.titleBar != null
? titleBar.merge(patch.titleBar)
: patch.titleBar != null ? patch.titleBar : titleBar);
: patch.titleBar != null ? patch.titleBar : titleBar,
patch.windowTransparent != null ? patch.windowTransparent : windowTransparent);
}
/// Converts this appearance to concrete launcher theme values.
@@ -309,6 +321,19 @@ public record ThemeAppearance(
}
}
/// Reads the optional window transparency field.
private static @Nullable Boolean readWindowTransparent(JsonObject object) {
JsonElement element = object.get(FIELD_WINDOW_TRANSPARENT);
if (element == null) {
return null;
}
if (!(element instanceof JsonPrimitive primitive) || !primitive.isBoolean()) {
LOG.warning("Ignored invalid theme windowTransparent: expected a boolean, got " + element);
return null;
}
return primitive.getAsBoolean();
}
/// Reads an optional string field.
private static @Nullable String readString(JsonObject object, String field) {
JsonElement element = object.get(field);
@@ -660,7 +660,8 @@ public final class ThemePackManager {
currentColorStyle(),
null,
background,
new ThemeTitleBar(currentTitleBarTransparent()));
new ThemeTitleBar(currentTitleBarTransparent()),
currentWindowTransparent());
Theme theme = new Theme(
null,
null,
@@ -711,6 +712,9 @@ public final class ThemePackManager {
if (themeColorType == ThemeColorType.DEFAULT) {
return ThemeColorSource.DEFAULT;
}
if (themeColorType == ThemeColorType.SYSTEM) {
return ThemeColorSource.custom(Themes.getSystemThemeColor());
}
if (themeColorType == ThemeColorType.BACKGROUND) {
if (backgroundType == BackgroundType.THEME_COLOR) {
return ThemeColorSource.DEFAULT;
@@ -755,6 +759,13 @@ public final class ThemePackManager {
: Objects.requireNonNullElse(resolveCurrentTitleBarTransparent(currentResolveContext()), false);
}
/// Returns whether the current launcher window should be transparent.
private static boolean currentWindowTransparent() throws IOException {
return SettingsManager.settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)
? settings().windowTransparentProperty().get()
: Objects.requireNonNullElse(resolveCurrentWindowTransparent(currentResolveContext()), false);
}
/// Resolves the selected theme's controlled brightness.
///
/// @param context the condition resolution context
@@ -795,6 +806,16 @@ public final class ThemePackManager {
return appearance != null && appearance.titleBar() != null ? appearance.titleBar().transparent() : null;
}
/// Resolves the selected theme window transparency directive.
///
/// @param context the condition resolution context
/// @return the selected theme window transparency directive, or `null` when unavailable
/// @throws IOException if the selected theme pack cannot be read
public static @Nullable Boolean resolveCurrentWindowTransparent(ThemeResolveContext context) throws IOException {
@Nullable ThemeAppearance appearance = resolveCurrentThemeAppearance(context);
return appearance != null ? appearance.windowTransparent() : null;
}
/// Resolves the current selected theme color source without extracting any wallpaper pixels.
///
/// @param context the condition resolution context
@@ -32,9 +32,6 @@ import javafx.collections.SetChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BackgroundImage;
@@ -115,6 +112,9 @@ public final class Themes {
if (FXUtils.DARK_MODE != null) {
observables.add(FXUtils.DARK_MODE);
}
if (FXUtils.ACCENT_COLOR != null) {
observables.add(FXUtils.ACCENT_COLOR);
}
bind(observables.toArray(new Observable[0]));
}
@@ -195,6 +195,7 @@ public final class Themes {
}
return switch (themeColorType) {
case DEFAULT -> ThemeColor.DEFAULT;
case SYSTEM -> getSystemThemeColor();
case CUSTOM -> fallback;
case BACKGROUND -> resolveWallpaperThemeColor(fallback, backgroundType);
};
@@ -282,9 +283,11 @@ public final class Themes {
colorScheme
);
/// The current JavaFX launcher background.
private static final ReadOnlyObjectWrapper<Background> background = new ReadOnlyObjectWrapper<>(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
/// The current JavaFX launcher background and its node opacity.
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
new LauncherBackground(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)),
1.0));
/// The last loaded background source used to rebuild the JavaFX background without reloading images.
private static @Nullable LoadedBackground loadedBackground;
@@ -300,8 +303,8 @@ public final class Themes {
/// A loaded JavaFX background source and the wallpaper color extracted while loading it.
private sealed interface LoadedBackground {
/// Returns the loaded JavaFX background.
Background background();
/// Returns the loaded JavaFX background and its node opacity.
LauncherBackground background();
/// Returns whether this background is the configured fallback background.
boolean fallbackBackground();
@@ -316,7 +319,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color extracted from [#image], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Image(
Background background,
LauncherBackground background,
javafx.scene.image.Image image,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -339,7 +342,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color represented by [#paint], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Paint(
Background background,
LauncherBackground background,
@Nullable javafx.scene.paint.Paint paint,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -359,7 +362,7 @@ public final class Themes {
/// @param background the loaded JavaFX background
/// @param fallbackBackground whether this background is the configured fallback background
record ThemeColorFill(
Background background,
LauncherBackground background,
boolean fallbackBackground) implements LoadedBackground {
/// Creates a loaded theme-color background.
///
@@ -528,6 +531,15 @@ public final class Themes {
return defaultBrightness = brightness;
}
/// Returns the current operating system accent color, or the launcher default when unavailable.
public static ThemeColor getSystemThemeColor() {
if (FXUtils.ACCENT_COLOR == null) {
return ThemeColor.DEFAULT;
}
@Nullable Color accentColor = FXUtils.ACCENT_COLOR.get();
return accentColor != null ? ThemeColor.of(accentColor) : ThemeColor.DEFAULT;
}
/// Returns the current resolved launcher theme.
public static ResolvedTheme getTheme() {
return theme.get();
@@ -543,8 +555,8 @@ public final class Themes {
return colorScheme.get();
}
/// Returns the current JavaFX launcher background property.
public static ReadOnlyObjectProperty<Background> backgroundProperty() {
/// Returns the current JavaFX launcher background and node opacity property.
public static ReadOnlyObjectProperty<LauncherBackground> backgroundProperty() {
startBackgroundUpdates();
return background.getReadOnlyProperty();
}
@@ -824,7 +836,8 @@ public final class Themes {
}
@Nullable LoadedBackground loaded = loadedBackground;
if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
Background newBackground = createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
LauncherBackground newBackground =
createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
loadedBackground = new LoadedBackground.ThemeColorFill(newBackground, themeColorFill.fallbackBackground());
background.set(newBackground);
}
@@ -858,17 +871,23 @@ public final class Themes {
LoadedBackground refreshed;
if (loaded instanceof LoadedBackground.Image imageBackground) {
refreshed = new LoadedBackground.Image(
createBackgroundWithOpacity(imageBackground.image(), opacity),
new LauncherBackground(
imageBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
imageBackground.image(),
imageBackground.wallpaperThemeColor(),
imageBackground.fallbackBackground());
} else if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
refreshed = new LoadedBackground.ThemeColorFill(
createThemeColorBackground(opacity),
new LauncherBackground(
themeColorFill.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
themeColorFill.fallbackBackground());
} else if (loaded instanceof LoadedBackground.Paint paintBackground) {
refreshed = new LoadedBackground.Paint(
createPaintBackground(paintBackground.paint(), opacity),
new LauncherBackground(
paintBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
paintBackground.paint(),
paintBackground.wallpaperThemeColor(),
paintBackground.fallbackBackground());
@@ -917,60 +936,37 @@ public final class Themes {
return FXUtils.loadImage(new CacheFileTask(url).run());
}
/// Creates a JavaFX paint background with the requested opacity.
private static Background createPaintBackground(@Nullable Paint paint, double opacity) {
opacity = MathUtils.clamp(opacity, 0., 1.);
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null) {
color = Color.WHITE;
}
if (opacity < 1.) {
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
}
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
} else {
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
}
/// Creates a JavaFX paint background with a separately stored node opacity.
private static LauncherBackground createPaintBackground(@Nullable Paint paint, double opacity) {
return new LauncherBackground(
new Background(new BackgroundFill(
Objects.requireNonNullElse(paint, Color.WHITE),
CornerRadii.EMPTY,
Insets.EMPTY)),
MathUtils.clamp(opacity, 0., 1.));
}
/// Creates a JavaFX background from the current theme color scheme surface container.
private static Background createThemeColorBackground(double opacity) {
private static LauncherBackground createThemeColorBackground(double opacity) {
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
}
/// Creates a JavaFX image background with the requested opacity.
private static Background createBackgroundWithOpacity(Image image, double opacity) {
PixelReader pixelReader = image.getPixelReader();
if (opacity <= 0) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
} else if (opacity >= 1. || pixelReader == null) {
return new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
} else {
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
PixelWriter pixelWriter = tempImage.getPixelWriter();
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
Color color = pixelReader.getColor(x, y);
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
pixelWriter.setColor(x, y, newColor);
}
}
return new Background(new BackgroundImage(
tempImage,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
}
/// Creates a JavaFX image background with a separately stored node opacity.
private static LauncherBackground createBackgroundWithOpacity(Image image, double opacity) {
Background imageBackground = new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(
BackgroundSize.AUTO,
BackgroundSize.AUTO,
false,
false,
false,
true)
));
return new LauncherBackground(imageBackground, MathUtils.clamp(opacity, 0., 1.));
}
/// Loads a default launcher background image from local workspace files.
@@ -1073,6 +1069,27 @@ public final class Themes {
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// Whether the launcher window should be transparent after applying launcher and theme settings.
private static final BooleanBinding windowTransparent = Bindings.createBooleanBinding(
() -> {
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)) {
return settings().windowTransparentProperty().get();
}
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
},
settings().windowTransparentProperty(),
settings().getThemeAppearanceOverrides(),
settings().selectedThemeProperty(),
settings().themeBrightnessModeProperty(),
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// The title text fill derived from the current color scheme.
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> titleBarTransparent.get()
@@ -1092,6 +1109,11 @@ public final class Themes {
return titleBarTransparent;
}
/// Returns whether the launcher window should be transparent after applying launcher and theme settings.
public static BooleanBinding windowTransparentProperty() {
return windowTransparent;
}
/// Returns whether the current color scheme uses dark brightness.
public static BooleanBinding darkModeProperty() {
return darkMode;
@@ -36,9 +36,7 @@ import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
@@ -73,6 +71,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -97,12 +96,12 @@ public final class Controllers {
public static final int MIN_CONTENT_HEIGHT = 450 + 2 + 40; // bg height + border width*2 + toolbar height
public static final int MIN_WIDTH = MIN_CONTENT_WIDTH + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final Screen SCREEN = Screen.getPrimary();
public static final Rectangle2D PRIMARY_SCREEN_BOUNDS = Screen.getPrimary().getBounds();
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageX = new SimpleDoubleProperty();
private static DoubleProperty stageY = new SimpleDoubleProperty();
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
private static final DoubleProperty contentX = new SimpleDoubleProperty();
private static final DoubleProperty contentY = new SimpleDoubleProperty();
private static final DoubleProperty contentWidth = new SimpleDoubleProperty();
private static final DoubleProperty contentHeight = new SimpleDoubleProperty();
private static Scene scene;
private static Stage stage;
@@ -133,12 +132,16 @@ public final class Controllers {
void run() throws Exception;
}
public static Scene getScene() {
return scene;
public static @Nullable Stage getStage() {
return stage;
}
public static Stage getStage() {
return stage;
public static ReadOnlyDoubleProperty windowWidthProperty() {
return contentWidth;
}
public static ReadOnlyDoubleProperty windowHeightProperty() {
return contentHeight;
}
@FXThread
@@ -214,64 +217,8 @@ public final class Controllers {
return decorator;
}
public static void saveWindowStates() {
saveWindowBounds();
}
private static void saveWindowBounds() {
if (stageX != null) {
state().setX(toContentX(stageX.get()) / SCREEN.getBounds().getWidth());
}
if (stageY != null) {
state().setY(toContentY(stageY.get()) / SCREEN.getBounds().getHeight());
}
if (stageHeight != null) {
state().setHeight(toContentHeight(stageHeight.get()));
}
if (stageWidth != null) {
state().setWidth(toContentWidth(stageWidth.get()));
}
}
public static void onApplicationStop() {
stageSizeChangeListener = null;
saveWindowBounds();
stageX = null;
stageY = null;
stageHeight = null;
stageWidth = null;
}
private static double toContentX(double stageX) {
return stageX + CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toContentY(double stageY) {
return stageY + CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toStageX(double contentX) {
return contentX - CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toStageY(double contentY) {
return contentY - CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toContentWidth(double stageWidth) {
return Math.max(0.0, stageWidth - CUSTOM_DECORATION_SHADOW_EXTENT);
}
private static double toContentHeight(double stageHeight) {
return Math.max(0.0, stageHeight - CUSTOM_DECORATION_SHADOW_EXTENT);
}
private static double toStageWidth(double contentWidth) {
return contentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
}
private static double toStageHeight(double contentHeight) {
return contentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
}
public static void initialize(Stage stage) {
@@ -285,7 +232,7 @@ public final class Controllers {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Screen.getPrimary().getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
@@ -293,52 +240,14 @@ public final class Controllers {
Controllers.stage = stage;
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty;
switch (sourceProperty.getName()) {
case "x": {
targetProperty = stageX;
break;
}
case "y": {
targetProperty = stageY;
break;
}
case "width": {
targetProperty = stageWidth;
break;
}
case "height": {
targetProperty = stageHeight;
break;
}
default: {
targetProperty = null;
}
}
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
) {
targetProperty.set(sourceProperty.get());
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
double initContentWidth = Math.max(MIN_CONTENT_WIDTH, state().getWidth());
double initContentHeight = Math.max(MIN_CONTENT_HEIGHT, state().getHeight());
double initWidth = toStageWidth(initContentWidth);
double initHeight = toStageHeight(initContentHeight);
double initWidth = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
double initHeight = initContentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
{
double initContentX = state().getX() * SCREEN.getBounds().getWidth();
double initContentY = state().getY() * SCREEN.getBounds().getHeight();
double initContentX = state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
double initContentY = state().getY() * PRIMARY_SCREEN_BOUNDS.getHeight();
boolean invalid = true;
double border = 20D;
@@ -355,24 +264,65 @@ public final class Controllers {
}
if (invalid) {
initContentX = (0.5D - initContentWidth / SCREEN.getBounds().getWidth() / 2)
* SCREEN.getBounds().getWidth();
initContentY = (0.5D - initContentHeight / SCREEN.getBounds().getHeight() / 2)
* SCREEN.getBounds().getHeight();
initContentX = (0.5D - initContentWidth / PRIMARY_SCREEN_BOUNDS.getWidth() / 2)
* PRIMARY_SCREEN_BOUNDS.getWidth();
initContentY = (0.5D - initContentHeight / PRIMARY_SCREEN_BOUNDS.getHeight() / 2)
* PRIMARY_SCREEN_BOUNDS.getHeight();
}
double initX = toStageX(initContentX);
double initY = toStageY(initContentY);
double initX = initContentX - CUSTOM_DECORATION_SHADOW_SIZE;
double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE;
stage.setX(initX);
stage.setY(initY);
stageX.set(initX);
stageY.set(initY);
contentX.set(initContentX);
contentY.set(initContentY);
}
stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
contentHeight.set(initContentHeight);
contentWidth.set(initContentWidth);
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o;
Stage currentStage = property.getBean() instanceof Stage s ? s : null;
if (currentStage == null)
return;
boolean saveState = !currentStage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|| !currentStage.isFullScreen() && !currentStage.isMaximized());
switch (property.getName()) {
case "x" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentX.set(value);
if (saveState)
state().setX(value / PRIMARY_SCREEN_BOUNDS.getWidth());
}
case "y" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentY.set(value);
if (saveState)
state().setY(value / PRIMARY_SCREEN_BOUNDS.getHeight());
}
case "width" -> {
double value = Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentWidth.set(value);
if (saveState)
state().setWidth(value);
}
case "height" -> {
double value = Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentHeight.set(value);
if (saveState)
state().setHeight(value);
}
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
stage.xProperty().addListener(weakListener);
stage.yProperty().addListener(weakListener);
stage.heightProperty().addListener(weakListener);
@@ -585,7 +535,7 @@ public final class Controllers {
/// Shows a warning that confirms backing up a read-only settings file before overwriting it.
///
/// @param text the file-specific read-only warning
/// @param text the file-specific read-only warning
/// @param overwrite the action that backs up and overwrites the file
public static void confirmBackupAndOverwrite(String text, ThrowingRunnable overwrite) {
dialog(new MessageDialogPane.Builder(
@@ -694,6 +644,22 @@ public final class Controllers {
decorator.showToast(content);
}
public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {
return FileUtils.toPath(directoryChooser.showDialog(stage));
}
public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showOpenDialog(stage));
}
public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showSaveDialog(stage));
}
public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage));
}
public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
switch (href) {
@@ -33,6 +33,7 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import kala.encdet.EncodingDetector;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
@@ -48,7 +49,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.*;
@@ -149,7 +149,7 @@ public class GameCrashWindow extends Stage {
String log;
try {
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
log = EncodingDetector.MODERN_WEB.readString(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
@@ -338,6 +338,13 @@ public class InstallerItem extends Control {
throw new AssertionError("Unknown state type: " + state.getClass());
}
}, control.resolvedStateProperty));
FXUtils.onChangeAndOperate(
control.resolvedStateProperty,
state -> statusLabel.pseudoClassStateChanged(
PseudoClass.getPseudoClass("incompatible"),
state instanceof IncompatibleState
)
);
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
@@ -164,6 +164,7 @@ public final class LogWindow extends Stage {
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final BooleanProperty wrapText = new SimpleBooleanProperty(true);
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXButton btnAlwaysOnTop = FXUtils.newToggleButton4(SVG.KEEP, 20);
@@ -174,7 +175,6 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getStyleClass().add("no-horizontal-scrollbar");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
@@ -331,11 +331,26 @@ public final class LogWindow extends Stage {
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
wrapTextProperty().addListener((obs, oldWrap, nowWrap) -> {
if (nowWrap) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
listView.getStyleClass().add("no-horizontal-scrollbar");
} else {
maxWidthProperty().unbind();
prefWidthProperty().unbind();
setMaxWidth(Region.USE_PREF_SIZE);
setPrefWidth(Region.USE_COMPUTED_SIZE);
listView.getStyleClass().remove("no-horizontal-scrollbar");
}
// only for horizontal scrollbar
listView.requestLayout();
});
}
setPadding(new Insets(2));
setWrapText(true);
wrapTextProperty().bind(control.wrapText);
setGraphic(null);
}
@@ -395,6 +410,10 @@ public final class LogWindow extends Stage {
autoScrollCheckBox.setSelected(true);
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXCheckBox wrapTextCheckBox = new JFXCheckBox(i18n("logwindow.wrap_text"));
wrapTextCheckBox.setSelected(true);
control.wrapText.bind(wrapTextCheckBox.selectedProperty());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
@@ -414,7 +433,7 @@ public final class LogWindow extends Stage {
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
control.getGameProcess().getProcess()
.onExit()
@@ -66,6 +66,7 @@ public enum SVG {
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_FILL("M4 20q-.825 0-1.4125-.5875T2 18v-12q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8v10q0 .825-.5875 1.4125T20 20H4Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
@@ -22,6 +22,8 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -34,8 +36,6 @@ import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -44,8 +44,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
setHeading(new Label(i18n("update.changelog")));
setBody(new JFXSpinner());
@@ -59,7 +59,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
// Downgrade update, no need to display changelog
return null;
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Document document = Jsoup.parse(WebURL.toURL(url), 30 * 1000);
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
if (node == null || !"h1".equals(node.nodeName())) {
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
@@ -151,7 +150,7 @@ public class AccountListItem extends RadioButton {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("account.skin.upload"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
return null;
}
@@ -121,6 +121,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
spinnerRefresh.setPrefSize(30.0, 30.0);
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot.tooltip"));
@@ -37,7 +37,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !settings().animationDisabledProperty().get();
private static final boolean ENABLED = !settings().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -29,7 +29,10 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
public class ComponentList extends Control implements NoPaddingComponent {
@@ -136,13 +139,25 @@ public class ComponentList extends Control implements NoPaddingComponent {
}
public static Node createComponentListTitle(String title) {
HBox node = new HBox();
return createComponentListTitle(title, null);
}
public static Node createComponentListTitle(String title, @Nullable String helpMessage) {
HBox node = new HBox(4);
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(8, 0, 0, 0));
{
Label advanced = new Label(title);
node.getChildren().setAll(advanced);
}
if (helpMessage != null) {
var helpIcon = new StackPane(SVG.HELP.createIcon(16));
FXUtils.installFastTooltip(helpIcon, helpMessage);
node.getChildren().add(helpIcon);
}
return node;
}
@@ -120,7 +120,7 @@ public class FileSelector extends HBox {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
@@ -131,7 +131,7 @@ public class FileSelector extends HBox {
private void openDirectoryChooser(JFXTextField customField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
Path dir = Controllers.showDialog(chooser);
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
@@ -23,8 +23,8 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
@@ -43,11 +43,10 @@ import org.jackhuang.hmcl.util.MathUtils;
public class FloatScrollBarSkin implements Skin<ScrollBar> {
private ScrollBar scrollBar;
private Region group;
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
private final Rectangle track = new Rectangle();
private final Rectangle thumb = new Rectangle();
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
private ChangeListener<Boolean> thumbHoverListener;
private BooleanBinding hoverOrPressed;
private Animation thumbHoverAnimation;
public FloatScrollBarSkin(final ScrollBar scrollBar) {
@@ -214,7 +213,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
}
};
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
this.hoverOrPressed = thumb.hoverProperty().or(thumb.pressedProperty());
FXUtils.onChangeAndOperate(hoverOrPressed, newValue -> {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
@@ -249,7 +249,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
thumbHoverListener = null;
hoverOrPressed = null;
scrollBar = null;
group = null;
}
@@ -26,10 +26,12 @@ import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jetbrains.annotations.Nullable;
@@ -274,6 +276,8 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
/// Updates the fixed preferred height after the row width or subtitle content changes.
private void updatePreferredHeight() {
if (FXUtils.getLimitHeight(this) > 0) return;
double width = getWidth();
if (width <= 0) {
setMinHeight(MIN_HEIGHT);
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -57,7 +57,9 @@ public class LineFileChooserButton extends LineButton {
public void fire() {
super.fire();
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
Scene scene = this.getScene();
Window owner = scene != null ? scene.getWindow() : null; // TODO: Allow user to set owner stage
String windowTitle = getFileChooserTitle();
Path initialDirectory = null;
@@ -53,8 +53,13 @@ public abstract class MDListCell<T> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) return;
updateControl(item, empty);
if (empty) {
setGraphic(null);
@@ -85,8 +85,8 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
group.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
Choice<T> choice = newValue != null ? choiceByToggle.get(newValue) : null;
selectedChoice.set(choice);
selectedValue.set(choice != null ? choice.getValue() : null);
selectedChoice.set(choice);
});
selectedValue.addListener((observable, oldValue, newValue) -> {
@@ -96,6 +96,12 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
});
}
/// Replaces the displayed choices.
@SafeVarargs
public final void setChoices(Choice<T>... choices) {
setChoices(List.of(choices));
}
/// Replaces the displayed choices.
public void setChoices(Collection<? extends Choice<T>> choices) {
this.choices.setAll(choices);
@@ -88,7 +88,7 @@ public interface TabControl {
setSelectedIndex(index);
Tab tab = getModelItem(index);
Tab<?> tab = getModelItem(index);
if (tab != null) {
setSelectedItem(tab);
}
@@ -106,7 +106,7 @@ public interface TabControl {
final int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
final Tab value = getModelItem(i);
final Tab<?> value = getModelItem(i);
if (value != null && value.equals(tab)) {
select(i);
return;
@@ -21,11 +21,12 @@ import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.*;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -67,16 +68,18 @@ public class TaskExecutorDialogPane extends BorderPane {
center.getChildren().setAll(lblTitle, taskListPane);
}
BorderPane bottom = new BorderPane();
this.setBottom(bottom);
HBox bottom = new HBox();
bottom.setAlignment(Pos.CENTER_LEFT);
bottom.setPadding(new Insets(0, 8, 8, 8));
bottom.setSpacing(8);
this.setBottom(bottom);
{
lblProgress = new Label();
bottom.setLeft(lblProgress);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
bottom.setRight(btnCancel);
bottom.getChildren().setAll(lblProgress, spacer, btnCancel);
}
setCancel(cancel);
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.theme.LauncherBackground;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
@@ -49,7 +50,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class Decorator extends Control {
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<>();
/// The launcher background and opacity rendered by this decorator's skin.
private final ObjectProperty<LauncherBackground> contentBackground = new SimpleObjectProperty<>();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
@@ -151,15 +154,18 @@ public class Decorator extends Control {
this.container.set(container);
}
public Background getContentBackground() {
/// Returns the launcher background rendered behind the content.
public LauncherBackground getContentBackground() {
return contentBackground.get();
}
public ObjectProperty<Background> contentBackgroundProperty() {
/// Returns the launcher background property rendered by this decorator's skin.
public ObjectProperty<LauncherBackground> contentBackgroundProperty() {
return contentBackground;
}
public void setContentBackground(Background contentBackground) {
/// Sets the launcher background rendered behind the content.
public void setContentBackground(LauncherBackground contentBackground) {
this.contentBackground.set(contentBackground);
}
@@ -44,6 +44,7 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.glavo.monetfx.ColorRole;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -134,9 +135,33 @@ public class DecoratorSkin extends SkinBase<Decorator> {
root.getChildren().setAll(shadowContainer);
StackPane wrapper = new StackPane();
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
() -> Themes.windowTransparentProperty().get()
? null
: new Background(new BackgroundFill(
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
CornerRadii.EMPTY,
Insets.EMPTY)),
Themes.windowTransparentProperty(),
Themes.colorSchemeProperty()));
Region backgroundNode = new Region();
backgroundNode.setMouseTransparent(true);
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
() -> skinnable.getContentBackground() == null
? null
: skinnable.getContentBackground().background(),
skinnable.contentBackgroundProperty()));
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
() -> skinnable.getContentBackground() == null
? 1.0
: skinnable.getContentBackground().opacity(),
skinnable.contentBackgroundProperty()));
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
BorderPane frame = new BorderPane();
frame.getStyleClass().addAll("jfx-decorator");
wrapper.getChildren().setAll(frame);
wrapper.getChildren().setAll(backgroundNode, frame);
skinnable.setDrawerWrapper(wrapper);
parent.getChildren().add(wrapper);
@@ -178,20 +203,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleContainer = new StackPane();
titleContainer.setPickOnBounds(false);
titleContainer.getStyleClass().addAll("jfx-tool-bar");
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
() -> Math.max(0.0, wrapper.getHeight()
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
wrapper.heightProperty(),
skinnable.titleTransparentProperty(),
titleContainer.heightProperty()));
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
// and decide whether the whole top bar should be rendered in white or black. TODO
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
if (titleTransparent) {
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
container.backgroundProperty().unbind();
container.setBackground(null);
titleContainer.getStyleClass().remove("background");
titleContainer.getStyleClass().add("gray-background");
} else {
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
wrapper.backgroundProperty().unbind();
wrapper.setBackground(null);
titleContainer.getStyleClass().add("background");
titleContainer.getStyleClass().remove("gray-background");
}
@@ -21,13 +21,14 @@ import com.jfoenix.controls.JFXButton;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jetbrains.annotations.NotNullByDefault;
@@ -48,18 +49,23 @@ public class GameDirectoryListItemSkin extends SkinBase<GameDirectoryListItem> {
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
SVGContainer left = (skinnable.isSelected() ? SVG.FOLDER_FILL : SVG.FOLDER).createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
root.setLeft(left);
FXUtils.onChangeAndOperate(skinnable.selectedProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
SVG targetIcon = active ? SVG.FOLDER_FILL : SVG.FOLDER;
if (left.getIcon() != targetIcon) {
left.setIcon(targetIcon, Motion.SHORT4);
}
});
FXUtils.onClicked(getSkinnable(), () -> GameDirectoryManager.setSelectedGameDirectory(skinnable.getGameDirectory()));
Node left = SVG.FOLDER.createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setPickOnBounds(false);
BorderPane.setAlignment(item, Pos.CENTER);
@@ -27,11 +27,14 @@ import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -112,6 +115,30 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
return new InstallersPageSkin(this);
}
/**
* Determines whether to display the extension pane.
* <p>
* This method controls the visibility of the extension pane in the UI.
* When this method returns {@code true} and the pane is visible, users can
* interact with the features inside it, which includes triggering the
* "Reset to Default Name" button handled by {@link #resetDefaultName()}.
* </p>
*
* @return {@code true} if the extension pane should be displayed; {@code false} otherwise.
*/
protected abstract boolean showExtendPane();
/**
* Resets the name to its default value.
* <p>
* This method contains the business logic for the "Reset to Default Name" button
* located within the extension pane. This logic can only be triggered by the user
* clicking the corresponding button after {@link #showExtendPane()} returns
* {@code true} and the extension pane is successfully displayed.
* </p>
*/
protected abstract void resetDefaultName();
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
/**
* Constructor for all SkinBase instances.
@@ -127,11 +154,27 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
{
HBox versionNamePane = new HBox(8);
versionNamePane.getStyleClass().add("card-non-transparent");
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.setStyle("-fx-padding: 20 16 20 16");
versionNamePane.setAlignment(Pos.CENTER_LEFT);
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
HBox.setHgrow(control.txtName, Priority.ALWAYS);
versionNamePane.getChildren().addAll(new Label(i18n("version.name")), control.txtName);
if (control.showExtendPane()) {
JFXButton clearButton = FXUtils.newToggleButton4(SVG.CLOSE);
FXUtils.installFastTooltip(clearButton, i18n("button.clear"));
clearButton.disableProperty().bind(control.txtName.textProperty().isEmpty().or(control.txtName.disableProperty()));
clearButton.setOnAction(e -> control.txtName.clear());
JFXButton resetButton = FXUtils.newToggleButton4(SVG.RESTORE);
FXUtils.installFastTooltip(resetButton, i18n("button.reset"));
resetButton.disableProperty().bind(control.txtName.disableProperty());
resetButton.setOnAction(e -> control.resetDefaultName());
versionNamePane.getChildren().addAll(clearButton, resetButton);
}
root.setTop(versionNamePane);
}
@@ -111,4 +111,13 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
@Override
public void cleanup(SettingsMap settings) {
}
@Override
protected boolean showExtendPane() {
return false;
}
@Override
protected void resetDefaultName() {
}
}
@@ -318,9 +318,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -143,4 +143,15 @@ public class InstallersPage extends AbstractInstallersPage {
txtName.setText(nameBuilder.toString());
isNameModifiedByUser = false;
}
@Override
protected boolean showExtendPane() {
return true;
}
@Override
protected void resetDefaultName() {
isNameModifiedByUser = false;
setTxtNameWithLoaders();
}
}
@@ -90,7 +90,7 @@ public final class LocalModpackPage extends ModpackPage {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
controller.onEnd();
return;
@@ -61,11 +61,12 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
{
var archiveNamePane = new LinePane();
{
FXUtils.setLimitHeight(archiveNamePane, 75);
archiveNamePane.setTitle(i18n("version.name"));
txtModpackName = new JFXTextField();
txtModpackName.setPrefWidth(300);
FXUtils.setLimitHeight(archiveNamePane, 75);
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setRight(txtModpackName);
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -112,7 +111,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
return;
@@ -1,95 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VanillaInstallWizardProvider implements WizardProvider {
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
public VanillaInstallWizardProvider(HMCLGameRepository repository) {
this.repository = repository;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = repository.getDependency(downloadProvider);
}
@Override
public void start(SettingsMap settings) {
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
settings.put(ModpackPage.REPOSITORY, repository);
}
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
settings.asStringMap().forEach((key, value) -> {
if (!"game".equals(key) && value instanceof RemoteVersion remoteVersion)
builder.version(remoteVersion);
});
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
})
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@Override
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.new_game.installation"));
settings.put("success_message", i18n("install.success"));
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@Override
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
() -> controller.onNext(new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@Override
public boolean cancel() {
return true;
}
}
@@ -213,6 +213,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
RemoteVersion oldRemoteVersion = getItem();
super.updateItem(remoteVersion, empty);
if (empty) {
@@ -221,6 +222,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
setGraphic(pane);
if (oldRemoteVersion == remoteVersion) return;
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
if (remoteVersion.getReleaseDate() != null) {
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
@@ -228,67 +231,65 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
twoLineListItem.setSubtitle(null);
}
if (oldRemoteVersion != remoteVersion) {
twoLineListItem.getTags().clear();
twoLineListItem.getTags().clear();
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
}
}
@@ -190,7 +190,7 @@ public final class ExportWizardProvider implements WizardProvider {
"OneSix",
exportInfo.getName() + "-" + exportInfo.getVersion(),
null,
Lang.toIntOrNull(setting.get(GameSettings::permSizeProperty)),
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
setting.getInheritable(GameSettings::commandWrapperProperty),
setting.getInheritable(GameSettings::preLaunchCommandProperty),
null,
@@ -47,7 +47,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
@@ -101,9 +100,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName);
minMemory.set(Optional.ofNullable(versionSetting.get(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.get(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.get(GameSettings::jvmOptionsProperty));
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
canIncludeLauncher = JarUtils.thisJarPath() != null;
}
@@ -118,7 +117,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
fileChooser.setInitialFileName(name.get() + ".zip");
}
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(fileChooser);
if (file == null) {
controller.onEnd();
return;
@@ -22,7 +22,10 @@ import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -32,11 +35,7 @@ import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
@@ -71,7 +70,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -107,6 +105,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
private boolean updatingParentSetting = false;
private final ObjectProperty<GameSettings.@Nullable Preset> activeParentSetting =
new SimpleObjectProperty<>(this, "activeParentSetting");
// GUI
private final ScrollPane scrollPane;
@@ -133,6 +133,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class;
this.isPresetSetting = settingType == GameSettings.Preset.class;
if (!isPresetSetting) {
bindActiveParentSetting();
}
this.scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -158,11 +161,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
{
if (isPresetSetting) {
var presetSettings = new ComponentList();
rootPane.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.type.global.preset")),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
basicSettings,
ComponentList.createComponentListTitle(
i18n("settings.type.global.preset"),
i18n("settings.type.global.preset.help")
),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.game")),
gameSettings,
ComponentList.createComponentListTitle(i18n("settings.launcher")),
@@ -191,6 +196,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
var parentGameSettingsPane = new LineSelectButton<GameSettings.@Nullable Preset>();
basicSettings.getContent().add(parentGameSettingsPane);
parentGameSettingsPane.setTitle(i18n("settings.type.global.preset"));
parentGameSettingsPane.setSubtitle(i18n("settings.type.global.preset.subtitle"));
parentGameSettingsPane.setConverter(setting -> setting != null
? PresetManagementPane.getPresetDisplayName(setting)
: i18n("settings.type.global.preset.default"));
@@ -274,9 +280,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refreshJavaSettings();
});
SettingsManager.getGameSettings().addListener(weakJavaListener);
settings().defaultGameSettingsPresetProperty().addListener(weakJavaListener);
activeParentSetting.addListener(weakJavaListener);
javaItem.selectedChoiceProperty().addListener((observable, oldChoice, newChoice) -> {
S setting = currentSetting.get();
if (setting == null || updatingSelectedJava) {
@@ -362,6 +366,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
// Memory Setting
@Nullable JFXButton autoMemoryButton = !isPresetSetting ? createInheritanceButton() : null;
ObjectProperty<Boolean> memorySelectedValue = new SimpleObjectProperty<>(true);
var memorySublist = new ComponentSublist(() -> {
var memoryItem = new RadioChoiceList<Boolean>();
memoryItem.setFallbackValue(true);
@@ -381,16 +386,18 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton = createInheritanceButton();
}
var options = new ArrayList<RadioChoiceList.Choice<Boolean>>();
options.add(new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true));
var manualMemoryEditor = new HBox(8);
manualMemoryEditor.setAlignment(Pos.CENTER_RIGHT);
manualMemoryEditor.getChildren().setAll(
maxMemorySlider,
maxMemoryTextField,
new Label(i18n("settings.memory.unit.mib")));
options.add(new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton));
memoryItem.setChoices(options);
memoryItem.setChoices(List.of(
new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true),
new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton)
));
memoryItem.selectedValueProperty().addListener((observable, oldValue, newValue) ->
memorySelectedValue.set(newValue));
var memoryStatusBar = new MemoryStatusBar();
@@ -418,7 +425,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
return List.of(memoryItem, memoryStatusPane);
});
@@ -428,7 +436,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
gameSettings.getContent().add(memorySublist);
memorySublist.setTitle(i18n("settings.memory"));
memorySublist.setHasSubtitle(true);
memorySublist.setDescription(i18n("settings.memory.auto_allocate"));
memorySelectedValue.addListener((observable, oldValue, newValue) ->
memorySublist.setDescription(getMemoryAllocationDisplayName(newValue)));
memorySublist.setDescription(getMemoryAllocationDisplayName(memorySelectedValue.get()));
bindMemorySelectedValue(memorySelectedValue);
// Launcher Visibility Setting
var launcherVisibilityPane = createInheritableButton(
@@ -837,18 +848,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private void bindInstanceParentSetting(LineSelectButton<GameSettings.@Nullable Preset> button) {
ObservableList<GameSettings.Preset> items = FXCollections.observableArrayList();
ObservableList<GameSettings.Preset> presets = getAvailableParentPresets();
InvalidationListener updateItems = observable -> {
@Nullable GameSettings.Preset selected = button.getValue();
items.setAll((GameSettings.Preset) null);
items.addAll(SettingsManager.getGameSettings());
items.addAll(presets);
refreshInstanceParentSettingConverter(button);
if (selected != null && SettingsManager.getGameSettings(selected.idProperty().getValue()) == null) {
if (selected != null && getAvailableParentPreset(selected.idProperty().getValue()) == null) {
button.setValue(null);
}
};
InvalidationListener weakUpdateItems = holder.weak(updateItems);
updateItems.invalidated(SettingsManager.getGameSettings());
SettingsManager.getGameSettings().addListener(weakUpdateItems);
updateItems.invalidated(presets);
presets.addListener(weakUpdateItems);
button.setItems(items);
refreshInstanceParentSettingConverter(button);
@@ -865,7 +877,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
try {
refreshInstanceParentSettingConverter(button);
GameSettingsPresetID parent = setting.parentProperty().getValue();
button.setValue(parent != null ? SettingsManager.getGameSettings(parent) : null);
button.setValue(parent != null ? getAvailableParentPreset(parent) : null);
} finally {
updatingParentSetting = false;
}
@@ -880,6 +892,49 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
: i18n("settings.type.global.preset.default"));
}
/// Returns the presets that can be selected as an instance parent.
private ObservableList<GameSettings.Preset> getAvailableParentPresets() {
return SettingsManager.gameSettingsPresets().getPresets();
}
/// Returns the preset selected by an instance parent ID.
private GameSettings.@Nullable Preset getAvailableParentPreset(@Nullable GameSettingsPresetID id) {
return SettingsManager.gameSettingsPresets().getPreset(id);
}
/// Keeps [#activeParentSetting] synchronized with the current instance's resolved parent preset.
private void bindActiveParentSetting() {
InvalidationListener refresh = observable -> refreshActiveParentSetting();
InvalidationListener weakRefresh = holder.weak(refresh);
getAvailableParentPresets().addListener(weakRefresh);
SettingsManager.settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
if (newValue != null) {
newValue.addListener(weakRefresh);
}
refreshActiveParentSetting();
});
S setting = currentSetting.get();
if (setting != null) {
setting.addListener(weakRefresh);
}
refreshActiveParentSetting();
}
/// Refreshes the current instance's resolved parent preset.
private void refreshActiveParentSetting() {
activeParentSetting.set(currentSetting.get() instanceof GameSettings.Instance instance
? getEffectiveParentGameSettings(instance)
: null);
}
/// Adds the title-line inheritance button for the Java selection sublist.
private void bindJavaInheritanceButton(ComponentSublist sublist) {
if (isPresetSetting) {
@@ -1033,7 +1088,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds an integer text field to a setting property with independent override state.
@@ -1050,7 +1106,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds the windowed resolution selector to the width and height settings.
@@ -1093,6 +1150,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable Boolean> focusedListener = (observable, oldValue, newValue) -> {
if (!newValue) {
@@ -1178,8 +1236,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
widthProperty.addListener(weakPropertyListener);
heightProperty.addListener(weakPropertyListener);
}
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
propertyListener.invalidated(setting);
}
@@ -1416,7 +1472,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1434,6 +1494,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1499,9 +1560,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1524,7 +1582,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1542,6 +1604,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1607,9 +1670,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1660,6 +1720,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -1728,6 +1790,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
GameSettings setting = currentSetting.get();
@@ -1747,6 +1810,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
event.consume();
});
@@ -1766,6 +1832,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
} finally {
updating.value = false;
}
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -1779,9 +1847,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting instanceof GameSettings.Instance instance) {
instance.addListener(weakRefresh);
@@ -1825,13 +1890,96 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
}
/// Keeps a listener attached to the current instance's parent preset setting property.
private <T extends @UnknownNullability Object> void updateParentSettingPropertyListener(
@Nullable GameSettings setting,
ObjectProperty<@Nullable SettingProperty<T>> activeParentProperty,
Function<GameSettings, ? extends SettingProperty<T>> propertyGetter,
InvalidationListener listener) {
SettingProperty<T> oldParentProperty = activeParentProperty.get();
SettingProperty<T> newParentProperty = setting instanceof GameSettings.Instance instance
? propertyGetter.apply(getEffectiveParentGameSettings(instance))
: null;
if (oldParentProperty == newParentProperty) {
return;
}
if (oldParentProperty != null) {
oldParentProperty.removeListener(listener);
}
activeParentProperty.set(newParentProperty);
if (newParentProperty != null) {
newParentProperty.addListener(listener);
}
}
/// Binds the memory sublist description source to the effective automatic memory setting.
private void bindMemorySelectedValue(ObjectProperty<Boolean> memorySelectedValue) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
S setting = currentSetting.get();
updateParentSettingPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
refreshHolder.value);
if (setting == null) {
memorySelectedValue.set(true);
return;
}
memorySelectedValue.set(resolveEffectiveSetting(setting)
.getInheritable(GameSettings::autoMemoryProperty));
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
SettingProperty<Boolean> oldAutoMemoryProperty = activeAutoMemoryProperty.get();
if (oldAutoMemoryProperty != null) {
oldAutoMemoryProperty.removeListener(weakRefresh);
}
SettingProperty<Boolean> newAutoMemoryProperty = newValue != null
? newValue.autoMemoryProperty()
: null;
activeAutoMemoryProperty.set(newAutoMemoryProperty);
if (newValue != null) {
newValue.addListener(weakRefresh);
}
if (newAutoMemoryProperty != null) {
newAutoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(newValue);
});
S setting = currentSetting.get();
if (setting != null) {
SettingProperty<Boolean> autoMemoryProperty = setting.autoMemoryProperty();
activeAutoMemoryProperty.set(autoMemoryProperty);
setting.addListener(weakRefresh);
autoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(setting);
}
/// Binds a radio choice list to an inheritable setting property.
private <T> void bindInheritableRadioChoiceList(
ComponentSublist sublist,
RadioChoiceList<T> item,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1841,6 +1989,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1857,6 +2010,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable T> itemListener = (observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1921,9 +2076,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
propertyListener.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<T> property = propertyGetter.apply(setting);
@@ -1937,7 +2089,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Binds the window type editor to the window type setting.
private void bindWindowSettings(ComponentSublist sublist, RadioChoiceList<GameWindowType> item) {
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeWindowTypeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeParentWindowTypeProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1947,6 +2101,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentWindowTypeProperty,
GameSettings::windowTypeProperty,
refreshHolder.value);
InheritableProperty<GameWindowType> property = activeWindowTypeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1963,6 +2122,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
item.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2022,9 +2183,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
activeWindowTypeProperty.set(setting.windowTypeProperty());
@@ -2057,8 +2215,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private <T> void bindInheritableSublistDescription(ComponentSublist sublist,
Function<GameSettings, InheritableProperty<T>> propertyGetter,
Function<T, String> converter) {
InvalidationListener propertyListener = observable -> initInheritableSublistDescription(sublist, propertyGetter, converter);
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener propertyListener = observable -> {
updateParentInheritablePropertyListener(
currentSetting.get(),
activeParentProperty,
propertyGetter,
refreshHolder.value);
initInheritableSublistDescription(sublist, propertyGetter, converter);
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
@@ -2069,16 +2238,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyGetter.apply(newValue).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
propertyGetter.apply(setting).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(setting);
}
private <T> void initInheritableSublistDescription(ComponentSublist sublist,
@@ -2093,6 +2259,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
sublist.setDescription(converter.apply(getEffectiveValue(setting, propertyGetter)));
}
/// Returns the localized memory allocation mode display name.
private static String getMemoryAllocationDisplayName(Boolean autoMemory) {
return i18n(Boolean.TRUE.equals(autoMemory)
? "settings.memory.auto_allocate"
: "settings.memory.manual_allocate");
}
private static String getWindowTypeDisplayName(GameWindowType type) {
return switch (type) {
case FULLSCREEN -> i18n("settings.game.window_type.fullscreen");
@@ -2139,7 +2312,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
button.setOverriddenTooltip(i18n("settings.game.override_global"));
button.setInheritAvailable(!isPresetSetting);
IndependentSettingBinder.bindToggleButton(currentSetting, button, propertyGetter, holder, this::getEffectiveParentGameSettings);
IndependentSettingBinder.bindToggleButton(
currentSetting,
button,
propertyGetter,
holder,
this::getEffectiveParentGameSettings,
activeParentSetting);
return button;
}
@@ -2160,7 +2339,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineSelectButton<T> button,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -2170,6 +2351,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2186,6 +2368,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.valueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2251,9 +2435,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2268,10 +2449,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineInheritableToggleButton button,
Function<GameSettings, InheritableProperty<Boolean>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2280,7 +2464,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = true;
try {
boolean overridden = isPropertyOverridden(setting, property);
button.setRawValue(overridden ? getDirectValue(property) : getEffectiveValue(setting, propertyGetter));
button.setRawValue(overridden
? getDirectValue(property)
: getEffectiveValue(setting, propertyGetter));
button.setOverridden(overridden);
button.setEffectiveValue(getEffectiveValue(setting, propertyGetter));
} finally {
@@ -2288,6 +2474,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
InheritableProperty<Boolean> property = activeProperty.get();
@@ -2338,9 +2526,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2409,7 +2594,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Returns the explicitly configured parent preset for an instance, falling back to the default preset.
private GameSettings.Preset getExplicitParentGameSettings(GameSettings.Instance instance) {
GameSettingsPresetID parent = instance.parentProperty().getValue();
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
GameSettings.Preset parentSetting = getAvailableParentPreset(parent);
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -28,7 +29,6 @@ import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.property.SettingProperty;
import org.jackhuang.hmcl.ui.MemoryStatusBar;
@@ -48,7 +48,6 @@ import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -69,7 +68,8 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<String>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<String>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
@@ -84,7 +84,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(setting, activeParentProperty, propertyGetter, parentGetter, refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -104,6 +109,7 @@ final class IndependentSettingBinder {
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -161,9 +167,12 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Integer>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton;
if (presetSetting) {
inheritButton = null;
@@ -174,6 +183,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Integer> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -193,6 +208,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -257,12 +274,16 @@ final class IndependentSettingBinder {
@Nullable JFXButton maxMemoryButton,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeMaxMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentMaxMemoryProperty = new SimpleObjectProperty<>();
Label physicalMemoryLabel = (Label) memoryStatusLabels.getLeft();
Label allocatedMemoryLabel = (Label) memoryStatusLabels.getRight();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
int totalMemoryMiB = Math.max(1, (int) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
maxMemorySlider.setValueFactory(slider -> Bindings.createStringBinding(
@@ -271,6 +292,18 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
parentGetter,
refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentMaxMemoryProperty,
GameSettings::maxMemoryProperty,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> autoMemoryProperty = activeAutoMemoryProperty.get();
SettingProperty<Integer> maxMemoryProperty = activeMaxMemoryProperty.get();
if (setting == null || autoMemoryProperty == null || maxMemoryProperty == null || updating.value) {
@@ -299,6 +332,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
choiceList.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -433,8 +468,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
memoryStatusBar.memoryStatusProperty().addListener(observable -> {
GameSettings setting = currentSetting.get();
if (setting == null) {
@@ -468,12 +501,21 @@ final class IndependentSettingBinder {
LineInheritableToggleButton button,
Function<GameSettings, SettingProperty<Boolean>> propertyGetter,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -483,7 +525,9 @@ final class IndependentSettingBinder {
try {
boolean overridden = isOverridden(setting, property);
Boolean effectiveValue = getEffectiveValue(setting, propertyGetter, parentGetter);
button.setRawValue(overridden ? getDirectValue(property) : Boolean.TRUE.equals(effectiveValue));
button.setRawValue(overridden
? getDirectValue(property)
: Boolean.TRUE.equals(effectiveValue));
button.setOverridden(overridden);
button.setEffectiveValue(Boolean.TRUE.equals(effectiveValue));
} finally {
@@ -491,6 +535,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -503,7 +549,8 @@ final class IndependentSettingBinder {
try {
setOverridden(setting, property, true);
property.setValue(newValue);
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -522,7 +569,8 @@ final class IndependentSettingBinder {
if (newValue) {
property.setValue(button.getRawValue());
}
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -569,47 +617,26 @@ final class IndependentSettingBinder {
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
memoryStatusBar.memoryAllocatedProperty().set(calculateAllocatedMemory(memoryStatusBar, autoMemory, maxMemory));
updateMemoryLabels(memoryStatusBar, physicalMemoryLabel, allocatedMemoryLabel, autoMemory, maxMemory);
}
private static double calculateAllocatedMemory(
MemoryStatusBar memoryStatusBar,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
return HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatusBar.getMemoryStatus().available(),
Boolean.TRUE.equals(autoMemory));
}
private static void updateMemoryLabels(
MemoryStatusBar memoryStatusBar,
Label physicalMemoryLabel,
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
boolean autoMemoryEnabled = Boolean.TRUE.equals(autoMemory);
long memoryAllocated;
if (Boolean.TRUE.equals(autoMemory)) {
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available());
} else if (maxMemory != null && maxMemory > 0) {
memoryAllocated = maxMemory * 1024L * 1024L;
} else {
memoryAllocated = GameSettings.SUGGESTED_MEMORY * 1024L * 1024L;
}
memoryStatusBar.memoryAllocatedProperty().set(memoryAllocated);
physicalMemoryLabel.setText(i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.total())));
allocatedMemoryLabel.setText(i18n(
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.available()
? (autoMemoryEnabled
? "settings.memory.allocate.auto.exceeded"
: "settings.memory.allocate.manual.exceeded")
: (autoMemoryEnabled
? "settings.memory.allocate.auto"
: "settings.memory.allocate.manual"),
GIGABYTES.convertFromBytes(maxMemoryBytes),
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatus.available(),
autoMemoryEnabled)),
GIGABYTES.convertFromBytes(memoryStatus.available())));
allocatedMemoryLabel.setText(
memoryStatus.hasAvailable() && memoryAllocated > memoryStatus.available()
? i18n("settings.memory.allocate.exceeded", GIGABYTES.convertFromBytes(memoryAllocated), GIGABYTES.convertFromBytes(memoryStatus.available()))
: i18n("settings.memory.allocate", GIGABYTES.convertFromBytes(memoryAllocated)));
}
private static <T> void toggleOverride(
@@ -656,9 +683,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
GameSettings setting = currentSetting.get();
if (setting != null) {
SettingProperty<T> property = propertyGetter.apply(setting);
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -136,14 +137,17 @@ public final class AboutPage extends SpinnerPane {
if (obj.has("image")) {
JsonElement image = obj.get("image");
if (image.isJsonPrimitive()) {
button.setLeading(loadImage(image.getAsString()));
var imageView = new ImageContainer(32, 32);
imageView.setImage(loadImage(image.getAsString()));
imageView.setMouseTransparent(true);
button.setLeading(imageView);
} else if (image.isJsonObject()) {
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
);
}));
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
)));
}
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -33,14 +34,15 @@ import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
@@ -64,10 +66,10 @@ public class DownloadSettingsPage extends StackPane {
downloadSource.getStyleClass().add("card-non-transparent");
{
Function<DownloadSource, String> converter = source -> switch (source) {
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
Function<DownloadSource, String> descriptionConverter = source -> {
String bundleKey = switch (source) {
case DEFAULT -> "download.provider.balanced.desc";
@@ -106,91 +108,83 @@ public class DownloadSettingsPage extends StackPane {
{
var downloadList = new ComponentList();
VBox downloadThreads = new VBox(16);
ComponentSublist fileCommonLocationSublist = new ComponentSublist(() -> {
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
return List.of(fileCommonLocation);
});
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
{
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
ComponentSublist downloadThreadsSublist = new ComponentSublist(() -> {
var downloadThreadsList = new RadioChoiceList<Boolean>();
downloadThreadsList.setChoices(
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.auto"), true),
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.custom"), false) {
@Override
protected Node createRightNode() {
HBox hbox = new HBox(8);
hbox.setViewOrder(-1);
hbox.setAlignment(Pos.CENTER);
// hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
downloadThreads.getChildren().add(chkAutoDownloadThreads);
var changedByTextField = new Holder<>(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.value = true;
slider.setValue(value.intValue());
changedByTextField.value = false;
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.value) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
settings().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
hbox.getChildren().setAll(slider, threadsField);
return hbox;
}
}
});
);
downloadThreadsList.selectedValueProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
return List.of(downloadThreadsList);
});
downloadThreadsSublist.setTitle(i18n("settings.launcher.download.threads"));
downloadThreadsSublist.descriptionProperty().bind(Bindings.createStringBinding(() -> {
if (settings().autoDownloadThreadsProperty().get()) {
return i18n("settings.launcher.download.threads.auto");
} else {
return Integer.toString(settings().downloadThreadsProperty().get());
}
}, settings().autoDownloadThreadsProperty(), settings().downloadThreadsProperty()));
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
hbox.getChildren().setAll(label, slider, threadsField);
downloadThreads.getChildren().add(hbox);
}
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(settings().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
downloadThreads.getChildren().add(hintPane);
}
}
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreadsSublist);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
}
@@ -232,6 +232,7 @@ public final class JavaDownloadDialog extends StackPane {
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
this.packageTypeBox.setConverter(FXUtils.stringConverter(JavaPackageType::getDisplayName));
this.downloadButton = new JFXButton(i18n("download"));
downloadButton.setOnAction(e -> onDownload());
@@ -25,6 +25,7 @@ import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
@@ -114,7 +115,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
@@ -226,6 +227,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
private static final class JavaItemCell extends ListCell<JavaRuntime> {
private final Node graphic;
private final Label label = new Label();
private final TwoLineListItem content;
private SVG removeIcon;
@@ -241,11 +243,17 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
label.setAlignment(Pos.CENTER);
label.setMinSize(24, 24);
label.setMaxSize(24, 24);
label.setPrefSize(24, 24);
label.setStyle("-fx-background-color: -monet-secondary-container; -fx-background-radius: 2; -fx-padding: 2; -fx-font-weight: normal; -fx-font-size: 12px;");
this.content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
BorderPane.setAlignment(content, Pos.CENTER);
center.getChildren().setAll(content);
center.getChildren().setAll(label, content);
root.setCenter(center);
HBox right = new HBox();
@@ -293,6 +301,9 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
if (empty || item == null) {
setGraphic(null);
} else {
int parsedVersion = item.getParsedVersion();
label.setText(parsedVersion >= 0 ? String.valueOf(parsedVersion) : "?");
content.setTitle((item.isJDK() ? "JDK" : "JRE") + " " + item.getVersion());
content.setSubtitle(item.getBinary().toString());
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.RotateTransition;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
@@ -29,6 +30,7 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
@@ -254,14 +256,37 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton = new JFXButton();
menuButton.getStyleClass().add("menu-button");
menuButton.setOnAction(e -> GameListPopupMenu.show(
menuButton.setOnAction(e -> {
JFXPopup popup = GameListPopupMenu.showAndGetPopup(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight(),
repository, versions
));
);
if (popup != null) {
Node graphic = menuButton.getGraphic();
if (graphic != null) {
if (AnimationUtils.isAnimationEnabled()) {
Duration duration = Duration.millis(200);
RotateTransition rotateOpen = new RotateTransition(duration, graphic);
rotateOpen.setToAngle(-180);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateOpen);
popup.setOnHidden(windowEvent -> {
RotateTransition rotateClose = new RotateTransition(duration, graphic);
rotateClose.setToAngle(0);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateClose);
});
} else {
graphic.setRotate(-180);
popup.setOnHidden(windowEvent -> graphic.setRotate(0));
}
}
}
});
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
@@ -23,6 +23,7 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -43,12 +44,7 @@ import javafx.stage.FileChooser;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorStyle;
import org.glavo.uuid.UUIDs;
import org.jackhuang.hmcl.setting.BackgroundType;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.LauncherSettings;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.ThemeColorType;
import org.jackhuang.hmcl.setting.UserSettings;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
import org.jackhuang.hmcl.theme.BuiltinBackground;
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
@@ -69,6 +65,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -213,6 +210,60 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
}
/// Binds a line toggle button to an inheritable theme appearance setting.
private static void bindThemeAppearanceToggleButton(
LineToggleButton button,
String setting,
BooleanProperty directProperty,
Supplier<Boolean> effectiveValueSupplier) {
JFXButton inheritButton = createThemeAppearanceOverrideButton();
button.setTitleTrailing(inheritButton);
Holder<Boolean> updating = new Holder<>(false);
InvalidationListener refresh = ignored -> {
if (updating.value) {
return;
}
updating.value = true;
try {
boolean overridden = settings().getThemeAppearanceOverrides().contains(setting);
button.setSelected(overridden ? directProperty.get() : effectiveValueSupplier.get());
updateThemeAppearanceOverrideButton(inheritButton, !overridden);
} finally {
updating.value = false;
}
};
button.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (updating.value) {
return;
}
updating.value = true;
try {
directProperty.set(Boolean.TRUE.equals(newValue));
settings().getThemeAppearanceOverrides().add(setting);
updateThemeAppearanceOverrideButton(inheritButton, false);
} finally {
updating.value = false;
}
refresh.invalidated(null);
});
directProperty.addListener(refresh);
addThemeAppearanceRefreshListener(refresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (!settings().getThemeAppearanceOverrides().contains(setting)) {
directProperty.set(button.isSelected());
settings().getThemeAppearanceOverrides().add(setting);
} else {
settings().getThemeAppearanceOverrides().remove(setting);
}
refresh.invalidated(null);
event.consume();
});
refresh.invalidated(null);
}
/// Returns the display name for the currently selected launcher theme.
private static String getSelectedThemeTitle() {
ThemeReference reference = settings().getSelectedThemeOrDefault();
@@ -291,20 +342,25 @@ public class PersonalizationPage extends StackPane {
String defaultPackId = "com.example.hmcl.theme-pack." + UUIDs.toCompactString(UUIDs.generateV7(exportTimestamp));
String defaultPackName = LocalDateTime.ofInstant(exportTimestamp, ZoneId.systemDefault()).format(EXPORTED_THEME_NAME_FORMATTER);
String userName = System.getProperty("user.name").trim();
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
String accountName = null;
{
var currentAccount = Accounts.getSelectedAccount();
if (currentAccount != null) accountName = currentAccount.getProfileName();
}
if (StringUtils.isBlank(accountName)) accountName = System.getProperty("user.name").trim();
String defaultAuthorName = StringUtils.isBlank(accountName) ? "Unknown" : accountName;
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.name"),
"")
defaultPackName)
.setPromptText(defaultPackName);
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.version"),
"")
ThemePackManager.CURRENT_THEME_PACK_VERSION)
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.author"),
"")
defaultAuthorName)
.setPromptText(defaultAuthorName);
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
@@ -323,14 +379,15 @@ public class PersonalizationPage extends StackPane {
private void exportCurrentThemePack(String packId, String version, String packName, String authorName) {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("theme_pack.export.title"));
String initialFileName = sanitizeThemePackFileName(packName) + ThemePackExporter.FILE_EXTENSION;
chooser.setInitialFileName(FileUtils.isNameValid(initialFileName)
? initialFileName
: "theme-pack" + ThemePackExporter.FILE_EXTENSION);
String initialFileName = sanitizeThemePackFileName(packName);
chooser.setInitialFileName(
(FileUtils.isNameValid(initialFileName) ? initialFileName : "theme-pack")
+ (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ThemePackExporter.FILE_EXTENSION)
);
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@Nullable Path output = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
@Nullable Path output = Controllers.showSaveDialog(chooser);
if (output == null) {
return;
}
@@ -443,6 +500,10 @@ public class PersonalizationPage extends StackPane {
i18n("settings.launcher.theme_color_type.default"),
ThemeColorType.DEFAULT);
var systemColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.system"),
ThemeColorType.SYSTEM);
var customColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.custom"),
ThemeColorType.CUSTOM) {
@@ -459,7 +520,11 @@ public class PersonalizationPage extends StackPane {
RadioChoiceList<ThemeColorType> themeColorChoiceList = new RadioChoiceList<>();
themeColorChoiceList.setFallbackValue(ThemeColorType.DEFAULT);
themeColorChoiceList.setChoices(Arrays.asList(defaultColorChoice, customColorChoice, backgroundColorChoice));
themeColorChoiceList.setChoices(Arrays.asList(
defaultColorChoice,
systemColorChoice,
customColorChoice,
backgroundColorChoice));
JFXButton themeColorOverrideButton = createThemeAppearanceOverrideButton();
themeColorSublist.setTitleRight(themeColorOverrideButton);
@@ -942,6 +1007,7 @@ public class PersonalizationPage extends StackPane {
Label textOpacity = new Label();
FXUtils.setLimitWidth(textOpacity, 50);
textOpacity.setAlignment(Pos.CENTER);
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
@@ -1076,6 +1142,25 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
themeAppearanceList.getContent().add(titleBarTransparentButton);
}
LineToggleButton windowTransparentButton = new LineToggleButton();
windowTransparentButton.setTitle(i18n("settings.launcher.window_transparent"));
bindThemeAppearanceToggleButton(
windowTransparentButton,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT,
settings().windowTransparentProperty(),
() -> {
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(
ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
});
themeAppearanceList.getContent().add(windowTransparentButton);
content.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")),
themeAppearanceList,
@@ -1087,7 +1172,8 @@ public class PersonalizationPage extends StackPane {
LineToggleButton animationButton = new LineToggleButton();
appearanceList.getContent().add(animationButton);
animationButton.selectedProperty().bindBidirectional(settings().animationDisabledProperty());
animationButton.setSelected(settings().isAnimationDisabled());
FXUtils.onChange(animationButton.selectedProperty(), value -> settings().animationDisabledProperty().set(value));
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
animationButton.setSubtitle(i18n("settings.take_effect_after_restart"));
@@ -22,8 +22,11 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -32,6 +35,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
@@ -41,7 +45,6 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.*;
import org.jackhuang.hmcl.ui.*;
@@ -159,7 +162,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@Nullable Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
@Nullable Path file = Controllers.showOpenDialog(chooser);
if (file == null) {
return;
}
@@ -415,8 +418,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
/// @param themePack the installed theme pack to display
private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.InstalledThemePack themePack) {
ThemePackManifest manifest = themePack.manifest();
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
HBox heading = new HBox(8);
heading.setAlignment(Pos.CENTER_LEFT);
@@ -481,7 +483,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setPrefViewportWidth(520);
scrollPane.setPrefViewportHeight(Math.min(360, manifest.themes().size() * 86));
scrollPane.maxHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.55));
scrollPane.maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.55));
StackPane body = new StackPane(scrollPane);
body.setPadding(new Insets(10, 0, 0, 0));
@@ -508,6 +510,9 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
/// Toolbar shown during normal browsing.
private final HBox toolbarNormal = new HBox();
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
/// Search input.
private final JFXTextField searchField = new JFXTextField();
@@ -545,13 +550,20 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
pause.setOnFinished(event ->
skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
pause.setRate(1);
pause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
changeToolbar(toolbarNormal);
searchField.clear();
pause.stop();
skinnable.filteredList.setPredicate(null);
isSearching.set(false);
});
onEscPressed(searchField, closeSearchBar::fire);
@@ -586,6 +598,15 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
listView.getStyleClass().add("no-horizontal-scrollbar");
ignoreEvent(listView, KeyEvent.KEY_PRESSED, event -> event.getCode() == KeyCode.ESCAPE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label();
placeholderLabel.textProperty().bind(
Bindings.when(isSearching).then(i18n("search.no_results_found")).otherwise("")
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
center.setContent(listView);
root.getContent().add(center);
}
@@ -597,6 +618,8 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
if (newToolbar == searchBar) {
runInFX(searchField::requestFocus);
isSearching.set(true);
}
}
}
@@ -29,7 +29,7 @@ import java.nio.file.Path;
/// @author Glavo
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
COMPRESSED("nbt", "dat", "dat_old") {
@Override
public Tag read(Path file) throws IOException {
return NBTCodec.of().readTag(file);
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.terracotta;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Binding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
@@ -56,6 +57,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@@ -84,15 +86,22 @@ public class TerracottaControllerPage extends StackPane {
});
}
@SuppressWarnings("FieldCanBeLocal")
private final Binding<Boolean> focusedBinding;
@SuppressWarnings("FieldCanBeLocal")
private final WeakListenerHolder holder = new WeakListenerHolder();
/* FIXME: It's sucked to have such a long logic, containing UI for all states defined in TerracottaState, with unclear control flows.
Consider moving UI into multiple files for each state respectively. */
public TerracottaControllerPage() {
holder.add(FXUtils.observeWeak(() -> {
focusedBinding = BindingMapping.of(this.sceneProperty())
.flatMap(it -> it != null ? it.windowProperty() : new SimpleObjectProperty<>())
.flatMap(it -> it != null ? it.focusedProperty() : null, () -> false);
FXUtils.onChange(focusedBinding, focused -> {
// Run daemon process only if HMCL is focused and is displaying current node.
TerracottaManager.switchDaemon(getScene() != null && Controllers.getStage().isFocused());
}, this.sceneProperty(), Controllers.getStage().focusedProperty()));
TerracottaManager.switchDaemon(getScene() != null && focused);
});
TransitionPane transition = new TransitionPane();
@@ -91,8 +91,7 @@ public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.Da
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res != null) {
installMultiDataPack(res);
}
@@ -25,6 +25,7 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -33,6 +34,7 @@ import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
@@ -51,7 +53,9 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
@@ -68,6 +72,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final TransitionPane toolbarPane;
@@ -79,10 +84,15 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final JFXListView<DataPackInfoObject> listView;
private final FilteredList<DataPackInfoObject> filteredList;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
private final JFXTextField searchField;
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
final Consumer<Integer> toggleSelect;
@@ -148,16 +158,21 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
searchPause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
isSearching.set(false);
searchField.clear();
searchPause.stop();
filteredList.setPredicate(null);
isSearching.set(false);
});
FXUtils.onEscPressed(searchField, closeSearchBar::fire);
searchBar.getChildren().addAll(searchField, closeSearchBar);
@@ -193,6 +208,23 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("datapack.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("datapack.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
this.listView.setItems(filteredList);
listView.getItems().addListener(listener);
@@ -514,15 +514,23 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
spinnerPane.failedReasonProperty().bind(
Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) {
return i18n("search.no_results_found");
} else {
return null;
}
},
getSkinnable().failedProperty(),
getSkinnable().loadingProperty(),
getSkinnable().pageCount,
getSkinnable().items)
);
spinnerPane.setOnFailedAction(e -> {
if (getSkinnable().retrySearch != null) {
if (getSkinnable().isFailed() && getSkinnable().retrySearch != null) {
getSkinnable().retrySearch.run();
}
});
@@ -46,8 +46,6 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -171,7 +169,7 @@ public class DownloadPage extends Control implements DecoratorPage {
fileChooser.setTitle(i18n("button.save_as"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension));
fileChooser.setInitialFileName(file.file().filename());
Path dest = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path dest = Controllers.showSaveDialog(fileChooser);
if (dest == null) {
return;
}
@@ -472,6 +470,9 @@ public class DownloadPage extends Control implements DecoratorPage {
case QUILT:
content.addTag(i18n("install.installer.quilt"));
break;
case LEGACY_FABRIC:
content.addTag(i18n("install.installer.legacyfabric"));
break;
}
}
@@ -557,8 +558,8 @@ public class DownloadPage extends Control implements DecoratorPage {
this.setActions(downloadButton, saveAsButton, cancelButton);
}
this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));
this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
onEscPressed(this, cancelButton::fire);
}
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.setting.VersionIconType;
@@ -35,25 +37,35 @@ public class GameAdvancedListItem extends AdvancedListItem {
private final ImageContainer imageContainer;
private final WeakListenerHolder holder = new WeakListenerHolder();
private HMCLGameRepository repository;
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<RefreshedVersionsEvent> onRefreshedVersionsListener;
public GameAdvancedListItem() {
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
imageContainer.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion));
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), it -> this.loadVersion()));
}
private void loadVersion(String version) {
if (GameDirectoryManager.getSelectedRepository() != repository) {
private void loadVersion() {
String version = GameDirectoryManager.getSelectedInstance();
boolean repositoryChanged = GameDirectoryManager.getSelectedRepository() != repository;
if (repositoryChanged) {
repository = GameDirectoryManager.getSelectedRepository();
if (repository != null) {
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion(repository.getSelectedInstance());
});
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
FXUtils.runInFX(this::loadVersion);
});
if (!repository.isLoaded()) {
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.registerWeak(event -> FXUtils.runInFX(this::loadVersion));
return;
}
}
if (version != null && repository != null && repository.hasVersion(version)) {
@@ -80,6 +80,7 @@ public final class GameListCell extends ListCell<GameListItem> {
{
HBox center = new HBox();
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
center.setMouseTransparent(true);
root.setCenter(center);
center.setPrefWidth(Region.USE_PREF_SIZE);
@@ -22,7 +22,8 @@ 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.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
@@ -51,9 +52,9 @@ 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.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
import org.jackhuang.hmcl.ui.directory.GameDirectoryPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -103,10 +104,8 @@ 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("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 + 12 * 2);
setLeft(pane, bottomLeftCornerList);
}
@@ -143,7 +142,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
GameDirectoryManager.registerVersionsListener(this::loadVersions);
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
setOnFailedAction(e -> Versions.addNewGame());
}
@FXThread
@@ -235,7 +234,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
searchBar.getChildren().setAll(searchField, closeSearchBar);
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("install.new_game"), SVG.DOWNLOAD, Versions::addNewGame), createToolbarButton2(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
@@ -51,6 +51,12 @@ public final class GameListPopupMenu extends StackPane {
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
}
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream()
.filter(it -> repository.hasVersion(it.getId()))
@@ -58,6 +64,8 @@ public final class GameListPopupMenu extends StackPane {
.toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
return popup;
}
private final JFXListView<GameItem> listView = new JFXListView<>();
@@ -137,7 +137,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) doInstallOffline(file);
}
@@ -17,17 +17,15 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.mod.ModManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.task.Schedulers;
@@ -52,8 +50,6 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.GameInstanceLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager;
@@ -92,14 +88,16 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId);
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(repository.getModManager(instanceId));
}
private void loadMods(ModManager modManager) {
setLoading(true);
if (this.modManager != modManager) {
getItems().clear();
}
this.modManager = modManager;
CompletableFuture.supplyAsync(() -> {
lock.lock();
@@ -112,6 +110,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
lock.unlock();
}
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
if (this.modManager != modManager) {
return;
}
updateSupportedLoaders(modManager);
if (exception == null) {
@@ -163,7 +165,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
// Sinytra Connector
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
@@ -173,7 +175,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res == null) return;
@@ -236,6 +238,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void checkUpdates(Collection<LocalModFile> mods) {
Objects.requireNonNull(mods);
if (isLoading()) {
return;
}
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = repository.getGameVersion(instanceId);
@@ -278,18 +284,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
}
public boolean isModded() {
return modded.get();
}
public BooleanProperty moddedProperty() {
return modded;
}
public void setModded(boolean modded) {
this.modded.set(modded);
}
public GameDirectory getGameDirectory() {
return this.repository.getGameDirectory();
}
@@ -21,8 +21,10 @@ import com.jfoenix.controls.*;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
@@ -35,12 +37,12 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.addon.*;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
@@ -61,6 +63,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
@@ -80,6 +83,7 @@ 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;
@NotNullByDefault
final class ModListPageSkin extends SkinBase<ModListPage> {
private final TransitionPane toolbarPane;
@@ -88,10 +92,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private final HBox toolbarSelecting;
private final JFXListView<ModInfoObject> listView;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final JFXTextField searchField;
@FXThread
private boolean isSearching = false;
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
@@ -101,6 +109,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
pane.getChildren().setAll(root);
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
listView.getStyleClass().add("no-horizontal-scrollbar");
@@ -118,19 +127,22 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchPause.setOnFinished(e -> search());
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
changeToolbar(toolbarNormal);
isSearching = false;
searchField.clear();
searchPause.stop();
isSearching.set(false);
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
});
@@ -192,7 +204,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(isSearching ? searchBar : toolbarNormal);
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
@@ -219,9 +231,26 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
listView.setCellFactory(x -> new ModInfoListCell(listView));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("mods.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("mods.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
skinnable.getItems().addListener((ListChangeListener<? super ModInfoObject>) c -> {
if (isSearching) {
if (isSearching.get()) {
search();
}
});
@@ -242,14 +271,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
root.getContent().add(center);
}
Label label = new Label(i18n("mods.not_modded"));
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
getChildren().setAll(pane);
}
@@ -264,7 +285,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
private void search() {
isSearching = true;
isSearching.set(true);
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
@@ -379,11 +400,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ModInfoDialog(ModInfoObject modInfo) {
HBox titleContainer = new HBox();
titleContainer.setSpacing(8);
titleContainer.setPadding(new Insets(0, 0, 12, 0));
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
DoubleBinding widthBinding = Controllers.windowWidthProperty().multiply(0.7);
prefWidthProperty().bind(widthBinding);
maxWidthProperty().bind(widthBinding);
var imageContainer = new ImageContainer(40);
titleContainer.setAlignment(Pos.CENTER_LEFT);
modInfo.loadIcon(imageContainer, null);
TwoLineListItem title = new TwoLineListItem();
@@ -392,13 +416,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
else
title.setTitle(modInfo.getModInfo().getName());
StringJoiner subtitle = new StringJoiner(" | ");
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
StringJoiner subtitle = new StringJoiner("\n");
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitle.add(modInfo.getModInfo().getGameVersion());
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitle.add(modInfo.getModInfo().getVersion());
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
@@ -418,7 +442,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
description.heightProperty().addListener((obs, oldVal, newVal) -> {
double maxHeight = stage.getHeight() * 0.5;
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});
@@ -522,7 +546,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(24);
ImageContainer imageContainer = new ImageContainer(32);
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
@@ -35,7 +35,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.addon.*;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
@@ -57,7 +56,7 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -176,7 +175,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("resourcepack.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
if (files != null && !files.isEmpty()) {
addFiles(files);
}
@@ -257,6 +256,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
}
@NotNullByDefault
private static final class ResourcePackListPageSkin extends SkinBase<ResourcePackListPage> {
private final JFXListView<ResourcePackInfoObject> listView;
private final JFXTextField searchField = new JFXTextField();
@@ -266,7 +266,11 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
private final HBox toolbarNormal = new HBox();
private final HBox toolbarSelecting = new HBox();
private boolean isSearching;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
private ResourcePackListPageSkin(ResourcePackListPage control) {
super(control);
@@ -309,19 +313,22 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchPause.setOnFinished(e -> search());
FXUtils.onChange(searchField.textProperty(), newValue -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
changeToolbar(toolbarNormal);
isSearching = false;
searchField.clear();
searchPause.stop();
isSearching.set(false);
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
});
@@ -346,7 +353,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(isSearching ? searchBar : toolbarNormal);
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
@@ -371,6 +378,23 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
listView.setCellFactory(x -> new ResourcePackListCell(listView, control));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("resourcepack.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("resourcepack.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
Bindings.bindContent(listView.getItems(), control.getItems());
listView.setOnContextMenuRequested(event -> {
@@ -403,7 +427,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
private void search() {
isSearching = true;
isSearching.set(true);
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
@@ -471,7 +495,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
private final ResourcePackListPage page;
private final JFXCheckBox checkBox;
private final ImageContainer imageContainer = new ImageContainer(24);
private final ImageContainer imageContainer = new ImageContainer(32);
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER);
private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO);
@@ -567,8 +591,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
HBox titleContainer = new HBox();
titleContainer.setSpacing(8);
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
ImageContainer imageContainer = new ImageContainer(40);
imageContainer.setImage(packInfoObject.getIcon());
@@ -594,7 +617,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
FXUtils.onChange(description.heightProperty(), newVal -> {
double maxHeight = stage.getHeight() * 0.5;
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});
@@ -149,7 +149,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
fileChooser.setTitle(i18n("schematics.add.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
i18n("extension.schematic"), "*.litematic"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
if (files != null && !files.isEmpty()) {
addFiles(files);
}
@@ -636,6 +636,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
SchematicsPageSkin() {
super(SchematicsPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("schematics.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Path;
@@ -76,7 +75,7 @@ public class VersionIconDialog extends DialogPane {
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile != null) {
try {
repository.setVersionIconFile(versionId, selectedFile);
@@ -17,11 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
@@ -29,8 +28,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.QuickPlayOption;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
@@ -92,6 +93,7 @@ public final class Versions {
}
Controllers.taskDialog(
new FileDownloadTask(downloadURLs, modpack)
.setName(file.name())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
ModpackInstallWizardProvider installWizardProvider;
@@ -263,7 +265,7 @@ public final class Versions {
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(chooser);
if (file != null) {
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
String defaultExt = getDefaultScriptExtension();
@@ -24,6 +24,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
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.layout.BorderPane;
@@ -162,6 +163,12 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
WorldBackupsPageSkin() {
super(WorldBackupsPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("world.backup.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -639,7 +639,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
fileChooser.setInitialFileName("icon.png");
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
Path iconPath = Controllers.showOpenDialog(fileChooser);
if (iconPath == null) return;
Image image;
@@ -27,6 +27,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
@@ -140,7 +141,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res == null || res.isEmpty()) return;
installWorld(res.get(0));
@@ -216,6 +217,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
WorldListPageSkin() {
super(WorldListPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("world.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -314,8 +321,13 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
@Override
protected void updateItem(World world, boolean empty) {
World oldWorld = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(world, empty);
if (oldWorld == world && oldEmpty == empty) return;
this.content.getTags().clear();
if (empty || world == null) {
@@ -74,7 +74,7 @@ public final class WorldManageUIUtils {
fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(fileChooser);
if (file == null) {
return;
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
@@ -43,10 +44,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -127,23 +128,21 @@ public final class UpdateHandler {
throw new IOException("Current JAR is not verified");
}
CompletableFuture<Void> future = new CompletableFuture<>();
var latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
Controllers.saveWindowStates();
SettingsManager.savePendingChanges();
} finally {
future.complete(null);
latch.countDown();
}
});
try {
future.get();
} catch (ExecutionException | InterruptedException ignored) {
latch.await();
} catch (InterruptedException ignored) {
// Ignore
}
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
@@ -222,13 +221,35 @@ public final class UpdateHandler {
commandline.add("-jar");
commandline.add(jar.toAbsolutePath().toString());
commandline.addAll(Arrays.asList(appArgs));
LOG.info("Starting process: " + commandline);
LOG.info("Starting process: " + maskCommandline(commandline));
new ProcessBuilder(commandline)
.directory(Paths.get("").toAbsolutePath().toFile())
.inheritIO()
.start();
}
private static String maskCommandline(List<String> commandline) {
return commandline.stream().map(str -> {
if (str.startsWith("-D")) {
int eqIdx = str.indexOf('=');
if (eqIdx != -1) {
String key = str.substring(2, eqIdx);
String value = str.substring(eqIdx + 1);
if (key.contains("http.proxy") ||
key.startsWith("https.proxy") ||
key.startsWith("socksProxy") ||
key.equals("hmcl.microsoft.auth.id") ||
key.equals("hmcl.curseforge.apikey")
) {
return "-D" + key + "=" + (value.isEmpty() ? "" : value.charAt(0) + "*".repeat(value.length() - 1));
}
}
}
return str;
}).collect(Collectors.joining(" "));
}
private static Optional<Path> tryRename(Path path, String newVersion) {
String filename = path.getFileName().toString();
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
@@ -21,6 +21,7 @@ import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.CrashWindow;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -101,7 +102,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.error("Unable to handle uncaught exception", handlingException);
}
FileSaver.shutdown();
SettingsManager.shutdown();
LOG.shutdown();
}
}
@@ -78,7 +78,7 @@ public final class NativePatcher {
JavaRuntime javaVersion,
GameSettings.Effective settings,
List<String> javaArguments) {
if (settings.get(GameSettings::useCustomNativesProperty)) {
if (settings.getInheritable(GameSettings::useCustomNativesProperty)) {
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
return version;
@@ -96,8 +96,8 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
final boolean useNativeGLFW = settings.get(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.get(GameSettings::useNativeOpenALProperty);
final boolean useNativeGLFW = settings.getInheritable(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.getInheritable(GameSettings::useNativeOpenALProperty);
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL)
&& gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) {
@@ -124,7 +124,7 @@ public final class NativePatcher {
Architecture arch = javaVersion.getArchitecture();
GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null;
if (settings.get(GameSettings::notPatchNativesProperty))
if (settings.getInheritable(GameSettings::notPatchNativesProperty))
return version;
if (arch.isX86() && (os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.MACOS))
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #282828;
-fixed-log-toggle-selected: #DEE2E6;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #FFFFFF;
-fixed-log-trace: #495057;
-fixed-log-debug: #343A40;
-fixed-log-info: #212529;
-fixed-log-warn: #331904;
-fixed-log-error: #58151C;
-fixed-log-fatal: #842029;
-fixed-log-selected: #6C757D;
-fixed-log-text-fill: #e3e3e3;
-fixed-log-trace-background: #495057;
-fixed-log-debug-background: #343A40;
-fixed-log-info-background: #212529;
-fixed-log-warn-background: #413c26;
-fixed-log-warn-text-fill: #fdf3aa;
-fixed-log-error-background: #4e3534;
-fixed-log-error-text-fill: #f9dedc;
-fixed-log-fatal-background: #842029;
-fixed-log-selected: #3e3e3e;
}
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #ffffff;
-fixed-log-toggle-selected: #000000;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #000000;
-fixed-log-trace: #EEE9E0;
-fixed-log-debug: #EEE9E0;
-fixed-log-info: #FFFFFF;
-fixed-log-warn: #FFEECC;
-fixed-log-error: #FFCCBB;
-fixed-log-fatal: #F7A699;
-fixed-log-selected: #C4C4C4;
-fixed-log-text-fill: #1f1f1f;
-fixed-log-trace-background: #EEE9E0;
-fixed-log-debug-background: #EEE9E0;
-fixed-log-info-background: #FFFFFF;
-fixed-log-warn-background: #fef6d5;
-fixed-log-warn-text-fill: #3e2f00;
-fixed-log-error-background: #fcebeb;
-fixed-log-error-text-fill: #410e0b;
-fixed-log-fatal-background: #F7A699;
-fixed-log-selected: #f2f2f2;
}
+33 -21
View File
@@ -396,6 +396,10 @@
-fx-cursor: hand;
}
.installer-item-status:incompatible {
-fx-opacity: 0.6;
}
.installer-item-wrapper {
-fx-background-color: -monet-surface;
-fx-background-radius: 4;
@@ -448,16 +452,24 @@
}
.launch-pane > .jfx-button.launch-button {
-fx-max-width: 207px;
-fx-min-width: 207px;
-fx-max-width: 200px;
-fx-min-width: 200px;
-fx-border-width: 0 3px 0 0;
-fx-border-color: -monet-on-surface-variant;
-fx-border-color: linear-gradient(
to bottom,
transparent 0%,
transparent 20%,
-monet-on-surface-variant 20%,
-monet-on-surface-variant 80%,
transparent 80%,
transparent 100%
);
-fx-background-radius: 4px 0 0 4px;
}
.launch-pane > .jfx-button.menu-button {
-fx-max-width: 20px;
-fx-min-width: 20px;
-fx-max-width: 27px;
-fx-min-width: 27px;
-fx-font-size: 15px;
-fx-background-radius: 0 4px 4px 0;
}
@@ -1382,27 +1394,27 @@
}
.log-toggle.fatal {
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-toggle.error {
-fx-background-color: -fixed-log-error;
-fx-background-color: -fixed-log-error-background;
}
.log-toggle.warn {
-fx-background-color: -fixed-log-warn;
-fx-background-color: -fixed-log-warn-background;
}
.log-toggle.info {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-toggle.debug {
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-toggle.trace {
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -1410,7 +1422,7 @@
}
.log-window {
-fx-background-color: -monet-surface-container;
-fx-background-color: -fixed-log-background;
}
.log-window .scroll-bar .thumb {
@@ -1433,36 +1445,36 @@
.log-window-list-cell:fatal {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-window-list-cell:error {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-error;
-fx-text-fill: -fixed-log-error-text-fill;
-fx-background-color: -fixed-log-error-background;
}
.log-window-list-cell:warn {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-warn;
-fx-text-fill: -fixed-log-warn-text-fill;
-fx-background-color: -fixed-log-warn-background;
}
.log-window-list-cell:info {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell:debug {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-window-list-cell:trace {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -487,7 +487,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
fatal.illegal_char=Your working directory 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" 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" or "Mesa Lavapipe" in "Global/Instance-specific Settings → Graphics 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.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
@@ -544,9 +544,6 @@ game.crash.reason.modmixin_failure=The game crashed because some mods failed to
This generally means that the mod has a bug or is incompatible with the current environment.\n\
\n\
You can check the log to find the error mod.
game.crash.reason.mod_repeat_installation=The game crashed because of duplicate mods.\n\
\n\
Each mod can only be installed once. Please delete the duplicate mod and then relaunch the game.
game.crash.reason.forge_error=Forge/NeoForge may have provided error information.\n\
\n\
You can view the log and make corresponding processing according to the log information in the error report.\n\
@@ -947,6 +944,7 @@ logwindow.terminate_game=Kill Game Process
logwindow.title=Log
logwindow.help=You can navigate to the HMCL community and find others for help.
logwindow.autoscroll=Auto-scroll
logwindow.wrap_text=Wrap Text
logwindow.export_game_crash_logs=Export Crash Logs
logwindow.export_dump=Export Game Stack Dump
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help.
@@ -1132,14 +1130,14 @@ modrinth.category.vanilla-like=Vanilla-like
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Worldgen
modrinth.category.8x-=8x-
modrinth.category.8x-=8x or lower
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.512x+=512x or higher
mods=Mods
mods.add=Add
@@ -1150,6 +1148,7 @@ mods.disable=Disable
mods.download=Download Mod
mods.download.title=Download Mod - %1s
mods.download.recommend=Recommended Mod Version - Minecraft %1s
mods.empty=No Mods
mods.enable=Enable
mods.game.version=Game Version
mods.manage=Mods
@@ -1183,6 +1182,7 @@ nbt.title=View File - %s
datapack=Data Packs
datapack.add=Add
datapack.add.title=Choose data pack archive you want to add
datapack.empty=No Data Packs
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
datapack.title=World [%s] - Data Packs
@@ -1201,6 +1201,7 @@ world.backup.create.new_one=New Backup
world.backup.create.failed=Failed to create backup.\n%s
world.backup.create.success=Successfully created a new backup: %s
world.backup.delete=Delete this backup
world.backup.empty=No Backup
world.backup.processing=Backing up ...
world.chunkbase=Chunk Base
world.chunkbase.end_city=End City
@@ -1220,6 +1221,7 @@ world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
world.download=Download
world.download.title=Download World - %1s
world.empty=No Worlds
world.export=Export the World
world.export.title=Choose the directory for this exported world
world.export.location=Save As
@@ -1294,6 +1296,7 @@ resourcepack.add.title=Choose resource pack archive you want to add
resourcepack.delete.failed=Failed to delete resource pack
resourcepack.download=Download
resourcepack.download.title=Download Resource Pack - %1s
resourcepack.empty=No Resource Packs
resourcepack.manage=Resource Packs
resourcepack.update_in_modpack.warning=Updating resource packs in a modpack can lead to irreparable results, possibly leading to incorrect textures and sounds. Are you sure you want to update?
resourcepack.warning.invalid=Invalid pack meta
@@ -1316,6 +1319,7 @@ schematics.create_directory.failed=Failed to create directory
schematics.create_directory.failed.already_exists=Directory already exists
schematics.create_directory.failed.empty_name=Name cannot be empty
schematics.create_directory.failed.invalid_name=Name contains invalid characters
schematics.empty=No Schematics
schematics.info.description=Description
schematics.info.enclosing_size=Enclosing Size
schematics.info.name=Name
@@ -1337,6 +1341,7 @@ search.sort=Sort By
search.first_page=First
search.previous_page=Previous
search.next_page=Next
search.no_results_found=No results found
search.last_page=Last
search.page_n=%1$d / %2$s
@@ -1535,7 +1540,7 @@ settings.game_directories.read_only=The game directory file was saved by a newer
settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience
settings.launcher.animation=Animation
settings.launcher.appearance=Appearance
settings.launcher.brightness=Theme Mode
@@ -1547,8 +1552,9 @@ settings.launcher.debug=Debug
settings.launcher.disable_april_fools=Do not enable April Fools features
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Threads
settings.launcher.download.threads=Download Threads
settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.custom=Custom
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
@@ -1604,16 +1610,16 @@ settings.launcher.theme_color_type.background=Follow Background Wallpaper
settings.launcher.theme_color_type.custom=Custom Color
settings.launcher.theme_color_type.custom.description=Use the color selected above.
settings.launcher.theme_color_type.default=Default
settings.launcher.theme_color_type.system=Follow System
settings.launcher.title_transparent=Transparent Titlebar
settings.launcher.turn_off_animations=Disable Animation
settings.launcher.version_list_source=Version List
settings.launcher.window_transparent=Transparent Window
settings.launcher.background.settings.opacity=Opacity
settings.memory=Memory
settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated
settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate.manual=%1$.1f GiB Allocated
settings.memory.allocate.manual.exceeded=%1$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate=%1$.1f GiB Allocated
settings.memory.allocate.exceeded=%1$.1f GiB Allocated (%2$.1f GiB Available)
settings.memory.auto_allocate=Automatically Allocate
settings.memory.lower_bound=Minimum Memory
settings.memory.manual_allocate=Manually Allocate Memory
@@ -1632,11 +1638,14 @@ settings.type.global.preset=Preset
settings.type.global.preset.create=Create Preset
settings.type.global.preset.default=Use Default Preset
settings.type.global.preset.duplicate_name=Preset name already exists
settings.type.global.preset.help=A game settings preset manages a set of game settings that you can adjust on this page.\n\
All instances inherit game settings from the current preset by default. You can override specific settings on the instance game settings page.
settings.type.global.preset.manage_all=Current Preset
settings.type.global.preset.name=Preset Name
settings.type.global.preset.rename=Rename Preset
settings.type.global.preset.remove=Delete Preset
settings.type.global.preset.remove.confirm=Are you sure you want to delete preset "%s"? Instances using this preset will fall back to the default preset.
settings.type.global.preset.subtitle=The current instance will inherit game settings from the specified preset
settings.type.global.preset.auto_name=Preset %d
shaderpack.download.title=Download Shader - %1s
@@ -407,7 +407,7 @@ extension.command=سكريبت شيل macOS
extension.datapack=أرشيف حزمة البيانات
extension.mod=ملف مود
extension.modloader=مثبّت محمّل المودات
extension.modloader.installer=مثبّت محمّل المودات
extension.resourcepack=أرشيف حزمة الموارد
extension.schematic=ملف مخطط
extension.world=أرشيف العالم
@@ -508,9 +508,6 @@ game.crash.reason.modmixin_failure=تعطّلت اللعبة لأن بعض ال
يعني هذا عموماً أن المود يحتوي على خطأ أو غير متوافق مع البيئة الحالية.\n\
\n\
يمكنك مراجعة السجل للعثور على المود المسبب للخطأ.
game.crash.reason.mod_repeat_installation=تعطّلت اللعبة بسبب مودات مكررة.\n\
\n\
يمكن تثبيت كل مود مرة واحدة فقط. يُرجى حذف المود المكرر ثم إعادة تشغيل اللعبة.
game.crash.reason.forge_error=قد يكون Forge/NeoForge قد أعطى معلومات عن الخطأ.\n\
\n\
يمكنك مراجعة السجل ومعالجة المشكلة وفقاً لمعلومات السجل في تقرير الخطأ.\n\
@@ -1428,10 +1425,8 @@ settings.launcher.version_list_source=قائمة الإصدارات
settings.launcher.background.settings.opacity=الشفافية
settings.memory=الذاكرة
settings.memory.allocate.auto=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص
settings.memory.allocate.auto.exceeded=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate.manual=%1$.1f جيجابايت مخصص
settings.memory.allocate.manual.exceeded=%1$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate=%1$.1f جيجابايت مخصص
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
settings.memory.auto_allocate=تخصيص تلقائي
settings.memory.lower_bound=الحد الأدنى للذاكرة
settings.memory.unit.mib=ميجابايت
@@ -110,7 +110,6 @@ account.methods.microsoft.purchase=Comprar Minecraft
account.methods.microsoft.snapshot.website=Sitio web oficial
account.methods.offline=Sin conexión
account.methods.offline.name.special_characters=Utilice solo letras, números y guiones bajos (máximo 16 caracteres)
account.methods.offline.name.invalid.tip=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.
account.methods.offline.name.invalid=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.\n\
\n\
\ · Algunos nombres de usuario legítimos: HuangYu, huang_Yu, Huang_Yu_123;\n\
@@ -577,9 +576,6 @@ game.crash.reason.modmixin_failure=El juego se ha colgado debido a que algunos m
Esto generalmente significa que el mod tiene un error o no es compatible con el entorno actual. \n\
\n\
Puede consultar el registro para ver si hay un mod incorrecto.
game.crash.reason.mod_repeat_installation=El juego se ha colgado debido a los mods duplicados.\n\
\n\
Cada mod sólo puede instalarse una vez. Elimine el mod duplicado y vuelva a ejecutar el juego.
game.crash.reason.forge_error=Forge/NeoForge puede haber proporcionado información errónea.\n\
\n\
Puede ver el registro y realizar las acciones correspondientes de acuerdo con la información de registro en el informe de errores.\n\
@@ -1037,14 +1033,8 @@ modrinth.category.vanilla-like=Similar a la vainilla
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Generación de mundos
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.8x-=8x o más bajo
modrinth.category.512x+=512x o mayor
mods=Mods
mods.add.failed=No se ha podido añadir el mod %s.
@@ -1329,10 +1319,8 @@ settings.launcher.version_list_source=Lista de versiones
settings.launcher.background.settings.opacity=Opacidad
settings.memory=Memoria
settings.memory.allocate.auto=%1$.1f GiB Mínimo / %2$.1f GiB Asignado
settings.memory.allocate.auto.exceeded=%1$.1f GiB Mínimo / %2$.1f GiB Asignado (%3$.1f GiB Disponible)
settings.memory.allocate.manual=%1$.1f GiB Asignados
settings.memory.allocate.manual.exceeded=%1$.1f GiB Asignados (%3$.1f GiB Disponibles)
settings.memory.allocate=%1$.1f GiB Asignados
settings.memory.allocate.exceeded=%1$.1f GiB Asignados (%2$.1f GiB Disponibles)
settings.memory.auto_allocate=Asignar automáticamente
settings.memory.lower_bound=Memoria mínima
settings.memory.unit.mib=MiB
@@ -363,7 +363,6 @@ game.crash.reason.mixin_apply_mod_failed=Mixin が %1$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 のログを確認できます。
game.crash.reason.mod_repeat_installation=現在のゲームには複数の同一の Mod が繰り返しインストールされており、各 Mod は 1 回しか表示できません。ゲームを開始する前に繰り返しの Mod を削除してください。
game.crash.reason.forge_error=Forge がエラー メッセージを表示した可能性があります。 \nログを表示し、エラー レポートのログ情報に従って対応するアクションを実行できます。 \nエラー メッセージが表示されない場合は、エラー レポートをチェックして、エラーがどのように発生したかを知ることができます。\n%1$s
game.crash.reason.mod_resolution0=mod の問題により、現在のゲームを続行できません。 \n間違った mod のログを確認できます。
game.crash.reason.java_version_is_too_high=Java のバージョンが高すぎて実行を継続できないため、現在のゲームがクラッシュしました。 \nゲームを起動する前に、グローバル ゲーム設定またはゲーム固有の設定の Java パス タブで Java の以前のバージョンに切り替えてください。 \nそうでない場合は、<a href="https://www.java.com/download/">java.com (Java8)</a> または <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java17)</a> およびその他のプラットフォームをダウンロードしてインストールします (インストール後にランチャーを再起動します)。
@@ -613,7 +612,6 @@ modpack.files.resourcepacks=リソース/テクスチャパック
modpack.files.saves=保存されたマップ
modpack.files.scripts=MineTweaker構成
modpack.files.servers_dat=サーバーリスト
modpack.install=%s modpackをインストールします
modpack.installing=modpackのインストール
modpack.introduction= Curse, Modrinth, MultiMC, MCBBS Modpackがサポートされています。
modpack.invalid=無効なmodpackファイル。
@@ -674,6 +672,8 @@ modrinth.category.utility=ユーティリティ
modrinth.category.worldgen=Worldgen
modrinth.category.datapack=Datapack
modrinth.category.folia=Folia
modrinth.category.8x-=8x以下
modrinth.category.512x+=512x以上
mods=Mods
mods.add.failed=mods %s の追加に失敗しました。
@@ -856,10 +856,8 @@ settings.launcher.title_transparent=タイトルの透明性
settings.launcher.version_list_source=バージョンリストソース
settings.memory=ゲームメモリ
settings.memory.allocate.auto=最小 %1$.1f GiB /実際の %2$.1f GiB
settings.memory.allocate.auto.exceeded=最小 %1$.1f GiB /実際の %2$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate.manual=割り当て %1$.1f GiB
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate=割り当て %1$.1f GiB
settings.memory.allocate.exceeded=割り当て %1$.1f GiB(%2$.1f GiB使用可能)
settings.memory.auto_allocate=メモリサイズの自動割り当て
settings.memory.lower_bound=最小メモリ
settings.memory.unit.mib=MiB
@@ -903,7 +903,6 @@ world.add.invalid=無可辨此緊囊
world.backup=司備
world.backup.create.new_one=建新備
world.backup.create.failed=建備未成。\n%s
world.backup.create.locked=是生界方見用,須閉戲而後行。
world.backup.create.success=新備既成:%s
world.backup.delete=刪是備
world.backup.processing=方備……
@@ -1110,7 +1109,7 @@ settings.icon=戲徵
settings.launcher=啟者置設
settings.launcher.allow_auto_agent=許改戲
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
settings.launcher.debug=勘誤
@@ -1152,10 +1151,8 @@ settings.launcher.version_list_source=版列供者
settings.launcher.background.settings.opacity=陰翳
settings.memory=戲憶
settings.memory.allocate.auto=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB
settings.memory.allocate.auto.exceeded=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate.manual=戲分之 %1$.1f GiB
settings.memory.allocate.manual.exceeded=戲分之 %1$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate=戲分之 %1$.1f GiB
settings.memory.allocate.exceeded=戲分之 %1$.1f GiB (械祇 %2$.1f GiB)
settings.memory.auto_allocate=自調其憶
settings.memory.lower_bound=至低憶界
settings.memory.unit.mib=兆二進字節
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more