Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d297417909 | ||
|
|
2854eac27d | ||
|
|
c9fd7c6a1c | ||
|
|
0e0517ab3c | ||
|
|
7a4bea10d0 | ||
|
|
b87d40ffcc | ||
|
|
1277bf8b5d | ||
|
|
c2e807055a | ||
|
|
9021ec1960 | ||
|
|
82a47e9e2d | ||
|
|
4841204fa0 | ||
|
|
897f5fe11b | ||
|
|
4657e3273e | ||
|
|
4edea5f5b9 | ||
|
|
36bd7d4491 | ||
|
|
3aa8603bd1 | ||
|
|
ca7a61e20a | ||
|
|
21afc81cf8 | ||
|
|
94a0e26be5 | ||
|
|
14e8d2d0b5 | ||
|
|
49c408cb23 | ||
|
|
b81c3398ac | ||
|
|
2855df3693 | ||
|
|
8184dc6cee | ||
|
|
df21559c0e | ||
|
|
73ff3081f8 | ||
|
|
fe05bb58a4 | ||
|
|
4c9cd950e4 | ||
|
|
646d9a09a5 | ||
|
|
255fe75640 | ||
|
|
de15dd8e98 | ||
|
|
886319a6c7 | ||
|
|
3320f6c341 | ||
|
|
b85be78166 | ||
|
|
a34a7439d7 | ||
|
|
591fd51600 | ||
|
|
c1407a9620 | ||
|
|
e0a91579f2 | ||
|
|
b68f404c3c | ||
|
|
c1f50f9ba3 | ||
|
|
d8bb2131f2 | ||
|
|
aca761385b | ||
|
|
15faccf376 | ||
|
|
241c9c419c | ||
|
|
4b36f8ce03 | ||
|
|
5c0baad782 | ||
|
|
850a56405c | ||
|
|
e8487c467b | ||
|
|
9866d88a16 | ||
|
|
af2d46ae89 | ||
|
|
6f7c561ecb | ||
|
|
93abe31acf | ||
|
|
a6f8659349 | ||
|
|
8728c89093 | ||
|
|
2dccd8099d | ||
|
|
d8a03cfed8 | ||
|
|
26a874a030 | ||
|
|
7c5c3e2fb4 | ||
|
|
62d7b36248 | ||
|
|
dec6837528 | ||
|
|
3484792e81 | ||
|
|
2b310e035d | ||
|
|
6e960612dd | ||
|
|
73a776b021 | ||
|
|
31a2f49ed2 | ||
|
|
ab268cfe48 | ||
|
|
721aeccfc0 | ||
|
|
e4348f727a | ||
|
|
dfc96f10da | ||
|
|
a5c7fe08d8 | ||
|
|
a6e0acca45 | ||
|
|
3d312f9247 | ||
|
|
18dc5b69c0 | ||
|
|
98af2c85b6 | ||
|
|
c4c0b1a1ce | ||
|
|
5e621ce340 | ||
|
|
1d1c8dfdc9 | ||
|
|
9b68b8825d | ||
|
|
5c17783223 | ||
|
|
a921be45f1 | ||
|
|
2dd1c0f85e | ||
|
|
7ba229b3e5 | ||
|
|
59a299630b | ||
|
|
b41b89e233 | ||
|
|
af137e5ff1 | ||
|
|
74ddbf5623 | ||
|
|
accef4a93f | ||
|
|
9c3a955fad |
No files matched your search
@@ -1,3 +0,0 @@
|
||||
code_review:
|
||||
pull_request_opened:
|
||||
code_review: false
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user