From 1aecac46df999c9bc031c4d933ee54c719b73794 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:06:38 +0800 Subject: [PATCH 01/16] Refactor: Replace FileUtils calls with Controllers methods for file dialog handling --- .../org/jackhuang/hmcl/ui/Controllers.java | 23 +++++++++++++++---- .../hmcl/ui/account/AccountListItem.java | 3 +-- .../hmcl/ui/construct/FileSelector.java | 4 ++-- .../ui/construct/LineFileChooserButton.java | 8 ++++--- .../hmcl/ui/download/LocalModpackPage.java | 2 +- .../ui/download/ModpackSelectionPage.java | 3 +-- .../hmcl/ui/export/ModpackInfoPage.java | 3 +-- .../hmcl/ui/main/JavaManagementPage.java | 2 +- .../hmcl/ui/main/PersonalizationPage.java | 2 +- .../hmcl/ui/main/ThemePackManagementPage.java | 2 +- .../terracotta/TerracottaControllerPage.java | 15 +++++++++--- .../hmcl/ui/versions/DataPackListPage.java | 3 +-- .../hmcl/ui/versions/DownloadPage.java | 3 +-- .../hmcl/ui/versions/InstallerListPage.java | 2 +- .../hmcl/ui/versions/ModListPage.java | 2 +- .../ui/versions/ResourcePackListPage.java | 3 +-- .../hmcl/ui/versions/SchematicsPage.java | 2 +- .../hmcl/ui/versions/VersionIconDialog.java | 3 +-- .../jackhuang/hmcl/ui/versions/Versions.java | 2 +- .../hmcl/ui/versions/WorldInfoPage.java | 2 +- .../hmcl/ui/versions/WorldListPage.java | 2 +- .../hmcl/ui/versions/WorldManageUIUtils.java | 2 +- 22 files changed, 56 insertions(+), 37 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 0c70fed237d..cad4ecd93fe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -36,9 +36,7 @@ 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.Nullable; import java.io.IOException; +import java.nio.file.Path; import java.time.LocalDate; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -585,7 +584,7 @@ public static void confirm(String text, String title, MessageType type, Runnable /// 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 +693,22 @@ public static void showToast(String content) { 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 showOpenMultipleDialog(FileChooser fileChooser) { + return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage)); + } + public static void onHyperlinkAction(String href) { if (href.startsWith("hmcl://")) { switch (href) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index fca58b099e3..9fbf4fbd5e9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -43,7 +43,6 @@ 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 Task uploadSkin() { 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; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileSelector.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileSelector.java index d86c88022bf..4eed3e44b15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileSelector.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileSelector.java @@ -120,7 +120,7 @@ private void openFileChooser(JFXTextField customField) { 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 @@ private void openFileChooser(JFXTextField customField) { 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java index 3e134a21787..6d587e70e15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java @@ -20,11 +20,11 @@ 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 @@ private String processPath(Path path) { 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index c2275a0d9bb..9fb130131c3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -90,7 +90,7 @@ public LocalModpackPage(WizardController controller) { 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index dc19d5ae391..423c8850c9e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -42,7 +42,6 @@ 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 @@ private void onChooseLocalFile() { 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java index a082e7e1e79..19c24657b50 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java @@ -47,7 +47,6 @@ 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; @@ -118,7 +117,7 @@ private void onNext() { 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java index 980ee636308..70261079b31 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java @@ -115,7 +115,7 @@ void onAddJava() { 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) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java index 7be7564d975..f21c769e892 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java @@ -387,7 +387,7 @@ private void exportCurrentThemePack(String packId, String version, String packNa 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; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java index 2c944584114..5337a69a7c3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java @@ -159,7 +159,7 @@ private void importThemePack() { 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; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java index c2a015eb2e6..7f95c3fefa6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java @@ -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.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 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() : null) + .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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java index 76d130b17a0..9ee38a15b0f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java @@ -91,8 +91,7 @@ public void add() { FileChooser chooser = new FileChooser(); chooser.setTitle(i18n("datapack.add.title")); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip")); - List res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage())); - + List res = Controllers.showOpenMultipleDialog(chooser); if (res != null) { installMultiDataPack(res); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index fb06c3ca0fe..21802a64fd5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -46,7 +46,6 @@ 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 +170,7 @@ public void saveAs(RemoteAddon.Version file) { 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; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java index ed554baa497..334115a6d7c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java @@ -137,7 +137,7 @@ public void loadInstance(HMCLGameRepository repository, String instanceId) { 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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 65db361eeed..3e68a0d7952 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -173,7 +173,7 @@ public void add() { FileChooser chooser = new FileChooser(); chooser.setTitle(i18n("mods.add.title")); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod")); - List res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage())); + List res = Controllers.showOpenMultipleDialog(chooser); if (res == null) return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 893f508073b..df26ed3018f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -57,7 +57,6 @@ 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.Nullable; import java.io.IOException; @@ -176,7 +175,7 @@ public void onAddFiles() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("resourcepack.add")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); - List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); + List files = Controllers.showOpenMultipleDialog(fileChooser); if (files != null && !files.isEmpty()) { addFiles(files); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 241c47a4351..a3237c4cf47 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -149,7 +149,7 @@ public void onAddFiles() { fileChooser.setTitle(i18n("schematics.add.title")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( i18n("extension.schematic"), "*.litematic")); - List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); + List files = Controllers.showOpenMultipleDialog(fileChooser); if (files != null && !files.isEmpty()) { addFiles(files); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java index 99378fa6869..ca72d231598 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java @@ -30,7 +30,6 @@ 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 VersionIconDialog(HMCLGameRepository repository, String versionId, Runnab 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index c986694163a..ee3695f5c70 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -263,7 +263,7 @@ public static void generateLaunchScript(HMCLGameRepository repository, String id ? 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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 2c508aebae7..935772a2772 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -639,7 +639,7 @@ private void changeWorldIcon() { 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index 6aa0b66ecd7..3d1cf3e10fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -140,7 +140,7 @@ public void add() { FileChooser chooser = new FileChooser(); chooser.setTitle(i18n("world.add.title")); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip")); - List res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage())); + List res = Controllers.showOpenMultipleDialog(chooser); if (res == null || res.isEmpty()) return; installWorld(res.get(0)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index c99e083d2d6..fbc13b55003 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -74,7 +74,7 @@ public static void export(World world, FileChannel sessionLockChannel) { 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; } From cddefb38172ac33ce82e9e15214477dadbbb0008 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:08:56 +0800 Subject: [PATCH 02/16] Refactor: Update saveWindowStates method to handle window state saving --- .../java/org/jackhuang/hmcl/ui/Controllers.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index cad4ecd93fe..b9ad9f90c4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -214,7 +214,18 @@ public static DecoratorController getDecorator() { } public static void saveWindowStates() { - 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())); + } } private static void saveWindowBounds() { @@ -234,7 +245,7 @@ private static void saveWindowBounds() { public static void onApplicationStop() { stageSizeChangeListener = null; - saveWindowBounds(); + saveWindowStates(); stageX = null; stageY = null; stageHeight = null; From a52b00fe0268add32ca4f2d42104d9876ea41ff4 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:28:12 +0800 Subject: [PATCH 03/16] Refactor: Simplify window state management and remove redundant methods --- .../org/jackhuang/hmcl/ui/Controllers.java | 122 ++++-------------- 1 file changed, 24 insertions(+), 98 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index b9ad9f90c4a..3e5a66a7ef7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -98,10 +98,10 @@ public final class Controllers { public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT; public static final Screen SCREEN = Screen.getPrimary(); 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 stageX = new SimpleDoubleProperty(); + private static final DoubleProperty stageY = new SimpleDoubleProperty(); + private static final DoubleProperty stageWidth = new SimpleDoubleProperty(); + private static final DoubleProperty stageHeight = new SimpleDoubleProperty(); private static Scene scene; private static Stage stage; @@ -214,74 +214,15 @@ public static DecoratorController getDecorator() { } public static void saveWindowStates() { - 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())); - } - } - - 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())); - } + state().setX(stageX.get()); + state().setY(stageX.get()); + state().setHeight(stageHeight.get()); + state().setWidth(stageWidth.get()); } public static void onApplicationStop() { stageSizeChangeListener = null; saveWindowStates(); - 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) { @@ -304,38 +245,23 @@ public static void initialize(Stage stage) { 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 + if (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()); + ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o; + switch (property.getName()) { + case "x" -> + stageX.set((property.get() + CUSTOM_DECORATION_SHADOW_SIZE) / SCREEN.getBounds().getWidth()); + case "y" -> + stageY.set((property.get() + CUSTOM_DECORATION_SHADOW_SIZE) / SCREEN.getBounds().getWidth()); + case "width" -> + stageWidth.set(Math.max(0.0, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT) / SCREEN.getBounds().getWidth()); + case "height" -> + stageHeight.set(Math.max(0.0, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT) / SCREEN.getBounds().getWidth()); + } } }; @@ -343,8 +269,8 @@ public static void initialize(Stage stage) { 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(); @@ -371,8 +297,8 @@ public static void initialize(Stage stage) { * SCREEN.getBounds().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); From 4e76144dfeab3814dc753b450c2dbc82613553c5 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:33:31 +0800 Subject: [PATCH 04/16] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 8 ++++++++ .../jackhuang/hmcl/ui/main/ThemePackManagementPage.java | 6 ++---- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 5 ++--- .../org/jackhuang/hmcl/ui/versions/ModListPageSkin.java | 8 +++----- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 3e5a66a7ef7..2b87cc8b95b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -140,6 +140,14 @@ public static Stage getStage() { return stage; } + public static ReadOnlyDoubleProperty windowWidthProperty() { + return stageWidth; + } + + public static ReadOnlyDoubleProperty windowHeightProperty() { + return stageHeight; + } + @FXThread public static VersionPage getVersionPage() { if (versionPage == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java index 5337a69a7c3..93496a9da76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java @@ -41,7 +41,6 @@ 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.*; @@ -415,8 +414,7 @@ private static final class ThemePackInfoDialog extends JFXDialogLayout { /// @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 +479,7 @@ private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.Insta 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)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 21802a64fd5..ad8de876b34 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -46,7 +46,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; @@ -556,8 +555,8 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s 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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 56a5e0321ba..be4f535c223 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -35,7 +35,6 @@ 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; @@ -381,9 +380,8 @@ final class ModInfoDialog extends JFXDialogLayout { titleContainer.setSpacing(8); titleContainer.setPadding(new Insets(0, 0, 12, 0)); - Stage stage = Controllers.getStage(); - prefWidthProperty().bind(stage.widthProperty().multiply(0.7)); - maxWidthProperty().bind(stage.widthProperty().multiply(0.7)); + prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7)); + maxWidthProperty().bind(Controllers.windowHeightProperty().multiply(0.7)); var imageContainer = new ImageContainer(40); titleContainer.setAlignment(Pos.CENTER_LEFT); @@ -421,7 +419,7 @@ final class ModInfoDialog extends JFXDialogLayout { 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); }); From a7a60cbfa014f59f73fdec8b422c3d4911cb420a Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:34:58 +0800 Subject: [PATCH 05/16] Refactor: Update width and height bindings to use window properties --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 2 +- HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java | 4 ++-- .../org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 2b87cc8b95b..b3f6541e1b6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -136,7 +136,7 @@ public static Scene getScene() { return scene; } - public static Stage getStage() { + public static @Nullable Stage getStage() { return stage; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java index cdafa7afb29..a9c7d7214bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java @@ -44,8 +44,8 @@ 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()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index df26ed3018f..6e4d03b20b6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -566,8 +566,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { 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()); From 47ebdf6fb9c9b6b7f41e4a10b520de883045c194 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:35:26 +0800 Subject: [PATCH 06/16] Refactor: Update maxHeight calculation to use Controllers for window height --- .../org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 6e4d03b20b6..f4d863c4a58 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -592,7 +592,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { 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); }); From 031d1e11b88757006998dab31e97c203239ba4c9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:35:53 +0800 Subject: [PATCH 07/16] Refactor: Remove unused Stage import from ResourcePackListPage --- .../org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index f4d863c4a58..45404c305f2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -35,7 +35,6 @@ 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; From ffe2b3d7dfd60e70c9084f84b60e774d37394a88 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 17 Jul 2026 22:36:11 +0800 Subject: [PATCH 08/16] Refactor: Remove unused getScene method from Controllers --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index b3f6541e1b6..5bb091cb396 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -132,10 +132,6 @@ public interface ThrowingRunnable { void run() throws Exception; } - public static Scene getScene() { - return scene; - } - public static @Nullable Stage getStage() { return stage; } From 5603be9242de2198dba032222ae3e5be8dae0ada Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 20:21:50 +0800 Subject: [PATCH 09/16] Refactor: Consolidate width binding calculations in ModListPageSkin --- .../org/jackhuang/hmcl/ui/versions/ModListPageSkin.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index be4f535c223..edf5602a310 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -21,6 +21,7 @@ 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.collections.ListChangeListener; @@ -380,8 +381,9 @@ final class ModInfoDialog extends JFXDialogLayout { titleContainer.setSpacing(8); titleContainer.setPadding(new Insets(0, 0, 12, 0)); - prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7)); - maxWidthProperty().bind(Controllers.windowHeightProperty().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); From c7205828eedd80caea6e6780057a357df7bc3f37 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 20:45:18 +0800 Subject: [PATCH 10/16] Refactor: Rename stage properties to content properties for clarity --- .../org/jackhuang/hmcl/ui/Controllers.java | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 5bb091cb396..f8bbae19e9b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -98,10 +98,10 @@ public final class Controllers { public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT; public static final Screen SCREEN = Screen.getPrimary(); private static InvalidationListener stageSizeChangeListener; - private static final DoubleProperty stageX = new SimpleDoubleProperty(); - private static final DoubleProperty stageY = new SimpleDoubleProperty(); - private static final DoubleProperty stageWidth = new SimpleDoubleProperty(); - private static final 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; @@ -137,11 +137,11 @@ public interface ThrowingRunnable { } public static ReadOnlyDoubleProperty windowWidthProperty() { - return stageWidth; + return contentWidth; } public static ReadOnlyDoubleProperty windowHeightProperty() { - return stageHeight; + return contentHeight; } @FXThread @@ -218,10 +218,10 @@ public static DecoratorController getDecorator() { } public static void saveWindowStates() { - state().setX(stageX.get()); - state().setY(stageX.get()); - state().setHeight(stageHeight.get()); - state().setWidth(stageWidth.get()); + state().setX(contentX.get() / SCREEN.getBounds().getWidth()); + state().setY(contentY.get() / SCREEN.getBounds().getHeight()); + state().setHeight(contentHeight.get()); + state().setWidth(contentWidth.get()); } public static void onApplicationStop() { @@ -257,14 +257,10 @@ public static void initialize(Stage stage) { ) { ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o; switch (property.getName()) { - case "x" -> - stageX.set((property.get() + CUSTOM_DECORATION_SHADOW_SIZE) / SCREEN.getBounds().getWidth()); - case "y" -> - stageY.set((property.get() + CUSTOM_DECORATION_SHADOW_SIZE) / SCREEN.getBounds().getWidth()); - case "width" -> - stageWidth.set(Math.max(0.0, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT) / SCREEN.getBounds().getWidth()); - case "height" -> - stageHeight.set(Math.max(0.0, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT) / SCREEN.getBounds().getWidth()); + case "x" -> contentX.set(property.get() + CUSTOM_DECORATION_SHADOW_SIZE); + case "y" -> contentY.set(property.get() + CUSTOM_DECORATION_SHADOW_SIZE); + case "width" -> contentWidth.set(Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT)); + case "height" -> contentHeight.set(Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT)); } } }; @@ -305,14 +301,14 @@ public static void initialize(Stage stage) { double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE; stage.setX(initX); stage.setY(initY); - stageX.set(initX); - stageY.set(initY); + contentX.set(initX); + contentY.set(initY); } stage.setHeight(initHeight); stage.setWidth(initWidth); - stageHeight.set(initHeight); - stageWidth.set(initWidth); + contentHeight.set(initHeight); + contentWidth.set(initWidth); stage.xProperty().addListener(weakListener); stage.yProperty().addListener(weakListener); stage.heightProperty().addListener(weakListener); From c95a718825c70d2fc1466979f1803a51eb545af9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 21:12:20 +0800 Subject: [PATCH 11/16] Refactor: Replace SCREEN constant with PRIMARY_SCREEN_BOUNDS for improved clarity --- .../org/jackhuang/hmcl/ui/Controllers.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index f8bbae19e9b..410c0e9ef1d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -96,7 +96,7 @@ 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 final DoubleProperty contentX = new SimpleDoubleProperty(); private static final DoubleProperty contentY = new SimpleDoubleProperty(); @@ -218,8 +218,8 @@ public static DecoratorController getDecorator() { } public static void saveWindowStates() { - state().setX(contentX.get() / SCREEN.getBounds().getWidth()); - state().setY(contentY.get() / SCREEN.getBounds().getHeight()); + state().setX(contentX.get() / PRIMARY_SCREEN_BOUNDS.getWidth()); + state().setY(contentY.get() / PRIMARY_SCREEN_BOUNDS.getHeight()); state().setHeight(contentHeight.get()); state().setWidth(contentWidth.get()); } @@ -240,7 +240,7 @@ public static void initialize(Stage stage) { 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"); } @@ -273,13 +273,13 @@ public static void initialize(Stage stage) { 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; for (Screen screen : Screen.getScreens()) { - Rectangle2D bound = screen.getBounds(); + Rectangle2D bound = PRIMARY_SCREEN_BOUNDS; if (bound.getMinX() + border <= initContentX + initContentWidth && initContentX <= bound.getMaxX() - border @@ -291,10 +291,10 @@ public static void initialize(Stage stage) { } 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 = initContentX - CUSTOM_DECORATION_SHADOW_SIZE; From e7befe13033cd77cba20842b81594411b8815e92 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 21:14:30 +0800 Subject: [PATCH 12/16] Refactor: Update screen bounds retrieval to use dynamic screen dimensions --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 410c0e9ef1d..da42eb8962d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -279,7 +279,7 @@ public static void initialize(Stage stage) { boolean invalid = true; double border = 20D; for (Screen screen : Screen.getScreens()) { - Rectangle2D bound = PRIMARY_SCREEN_BOUNDS; + Rectangle2D bound = screen.getBounds(); if (bound.getMinX() + border <= initContentX + initContentWidth && initContentX <= bound.getMaxX() - border From 74a8ce36d1f9d39812699b38d74ee4f69cbda5f6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 21:23:53 +0800 Subject: [PATCH 13/16] Refactor: Update content width and height bindings to use initialized content dimensions --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index da42eb8962d..5c7196dcf41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -307,8 +307,8 @@ public static void initialize(Stage stage) { stage.setHeight(initHeight); stage.setWidth(initWidth); - contentHeight.set(initHeight); - contentWidth.set(initWidth); + contentHeight.set(initContentHeight); + contentWidth.set(initContentWidth); stage.xProperty().addListener(weakListener); stage.yProperty().addListener(weakListener); stage.heightProperty().addListener(weakListener); From f78e6a4beffbe5ba97d0c709323fd53753a67a86 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 21:49:00 +0800 Subject: [PATCH 14/16] Refactor: Simplify window state management by integrating state saving into size change listener --- .../org/jackhuang/hmcl/ui/Controllers.java | 68 +++++++++++-------- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 19 ------ 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 5c7196dcf41..67fac873327 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -217,16 +217,8 @@ public static DecoratorController getDecorator() { return decorator; } - public static void saveWindowStates() { - state().setX(contentX.get() / PRIMARY_SCREEN_BOUNDS.getWidth()); - state().setY(contentY.get() / PRIMARY_SCREEN_BOUNDS.getHeight()); - state().setHeight(contentHeight.get()); - state().setWidth(contentWidth.get()); - } - public static void onApplicationStop() { stageSizeChangeListener = null; - saveWindowStates(); } public static void initialize(Stage stage) { @@ -248,25 +240,6 @@ public static void initialize(Stage stage) { Controllers.stage = stage; - stageSizeChangeListener = o -> { - if (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()) - ) { - ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o; - switch (property.getName()) { - case "x" -> contentX.set(property.get() + CUSTOM_DECORATION_SHADOW_SIZE); - case "y" -> contentY.set(property.get() + CUSTOM_DECORATION_SHADOW_SIZE); - case "width" -> contentWidth.set(Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT)); - case "height" -> contentHeight.set(Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT)); - } - } - }; - - 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 = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT; @@ -309,6 +282,47 @@ public static void initialize(Stage stage) { stage.setWidth(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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index ec28b67daff..6e500e1e082 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -43,8 +43,6 @@ 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.regex.Matcher; import java.util.regex.Pattern; @@ -127,23 +125,6 @@ public static void updateFrom(RemoteVersion version) { throw new IOException("Current JAR is not verified"); } - CompletableFuture future = new CompletableFuture<>(); - - Platform.runLater(() -> { - try { - Controllers.saveWindowStates(); - } finally { - future.complete(null); - } - }); - - try { - future.get(); - } catch (ExecutionException | InterruptedException ignored) { - // Ignore - } - - try { FileSaver.waitForAllSaves(); } catch (InterruptedException ignored) { From f5d5737dd1fda94ecbc36542ee57d18ec6ea4f01 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 18 Jul 2026 21:52:36 +0800 Subject: [PATCH 15/16] Refactor: Ensure window property fallback to a new SimpleObjectProperty for improved stability --- .../jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java index 7f95c3fefa6..e99dc3714ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java @@ -95,7 +95,7 @@ public class TerracottaControllerPage extends StackPane { Consider moving UI into multiple files for each state respectively. */ public TerracottaControllerPage() { focusedBinding = BindingMapping.of(this.sceneProperty()) - .flatMap(it -> it != null ? it.windowProperty() : null) + .flatMap(it -> it != null ? it.windowProperty() : new SimpleObjectProperty<>()) .flatMap(it -> it != null ? it.focusedProperty() : null, () -> false); FXUtils.onChange(focusedBinding, focused -> { From 3cd4a95644997e80f1167f1c7ce8b28f92e35bf3 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 19 Jul 2026 22:08:57 +0800 Subject: [PATCH 16/16] Refactor: Replace FileSaver with SettingsManager for improved shutdown handling and state persistence --- .../java/org/jackhuang/hmcl/EntryPoint.java | 4 +-- .../java/org/jackhuang/hmcl/Launcher.java | 2 +- .../hmcl/setting/JsonSettingFile.java | 9 +++++- .../jackhuang/hmcl/setting/LauncherState.java | 10 +++++++ .../hmcl/setting/SettingsManager.java | 9 ++++++ .../jackhuang/hmcl/util/CrashReporter.java | 3 +- .../hmcl/setting/LauncherStateTest.java | 30 +++++++++++++++++++ .../hmcl/util/gson/ObservableSetting.java | 25 ++++++++++++++++ .../hmcl/util/javafx/ObservableHelper.java | 4 ++- 9 files changed, 90 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/EntryPoint.java b/HMCL/src/main/java/org/jackhuang/hmcl/EntryPoint.java index 3ba178e2599..0e03b81346d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/EntryPoint.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/EntryPoint.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl; -import org.jackhuang.hmcl.util.FileSaver; +import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.util.SelfDependencyPatcher; import org.jackhuang.hmcl.util.SwingUtils; import org.jackhuang.hmcl.java.JavaRuntime; @@ -67,7 +67,7 @@ public static void main(String[] args) { } public static void exit(int exitCode) { - FileSaver.shutdown(); + SettingsManager.shutdown(); LOG.shutdown(); System.exit(exitCode); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index a2d85d03fb6..14afcce8998 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -266,7 +266,7 @@ private static void checkConfigInTempDir() { @Override public void stop() throws Exception { Controllers.onApplicationStop(); - FileSaver.shutdown(); + SettingsManager.shutdown(); LOG.shutdown(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/JsonSettingFile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/JsonSettingFile.java index 05d6d9fb4f1..14a3995ab92 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/JsonSettingFile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/JsonSettingFile.java @@ -156,7 +156,14 @@ private LoadResult result(T value, SettingFileAccess access) { /// /// @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. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherState.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherState.java index b2a40739f05..ba5af8a027c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherState.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherState.java @@ -19,6 +19,7 @@ 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 LauncherState() { 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 schema = new SimpleObjectProperty<>(CURRENT_SCHEMA); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/SettingsManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/SettingsManager.java index d980a15de3e..e8cf175ad1a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/SettingsManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/SettingsManager.java @@ -55,6 +55,15 @@ public final class SettingsManager { private SettingsManager() { } + /// Saves deferred launcher state changes before stopping asynchronous file persistence. + public static void shutdown() { + if (launcherState != null && launcherState.isSavable() && launcherState.isSavePending()) { + STATE_FILE.save(launcherState); + launcherState.setSavePending(false); + } + FileSaver.shutdown(); + } + /// The local directory storing per-workspace configuration files. private static final Path LOCAL_CONFIG_FILES_DIRECTORY = Metadata.HMCL_LOCAL_HOME.resolve("config"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index c100236792c..7cd54164d45 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -21,6 +21,7 @@ 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 void uncaughtException(Thread t, Throwable e) { LOG.error("Unable to handle uncaught exception", handlingException); } - FileSaver.shutdown(); + SettingsManager.shutdown(); LOG.shutdown(); } } diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/LauncherStateTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/LauncherStateTest.java index 4dc99dcb02c..194ec10c6c5 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/LauncherStateTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/LauncherStateTest.java @@ -23,12 +23,42 @@ import org.junit.jupiter.api.Test; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.*; /// Tests for detached launcher state migration. @NotNullByDefault public final class LauncherStateTest { + /// Tests that settings listeners receive the observable field that changed. + @Test + public void reportsChangedFieldToListeners() { + LauncherState state = new LauncherState(); + AtomicBoolean notified = new AtomicBoolean(); + state.addListener(source -> { + assertSame(state.xProperty(), source); + notified.set(true); + }); + + state.setX(0.25); + + assertTrue(notified.get()); + } + + /// Tests that only window geometry fields defer automatic persistence. + @Test + public void identifiesDeferredWindowGeometryFields() { + LauncherState state = new LauncherState(); + + assertFalse(state.shouldSaveImmediately(state.xProperty())); + assertFalse(state.shouldSaveImmediately(state.yProperty())); + assertFalse(state.shouldSaveImmediately(state.widthProperty())); + assertFalse(state.shouldSaveImmediately(state.heightProperty())); + assertTrue(state.shouldSaveImmediately(state.schemaProperty())); + assertTrue(state.shouldSaveImmediately(state.promptedVersionProperty())); + assertTrue(state.shouldSaveImmediately(state.getShownTips())); + } + /// Tests extracting runtime state fields from a legacy config object. @Test public void extractsLauncherStateFromLegacyConfigJson() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableSetting.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableSetting.java index a2b423c9925..7971f5818e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableSetting.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableSetting.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.javafx.DirtyTracker; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.NotNullByDefault; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -57,6 +58,7 @@ /// All subclasses of this class must call [#register()] once in their constructor. /// /// @author Glavo +@NotNullByDefault public abstract class ObservableSetting implements Observable { private static final ClassValue>> FIELDS = new ClassValue<>() { @@ -94,6 +96,9 @@ protected List> computeValue(@NotNull Class type protected transient final Map unknownFields = new HashMap<>(); protected transient final DirtyTracker tracker = new DirtyTracker(); + /// Whether a deferred field change still needs to be saved. + private transient boolean savePending; + private boolean registered = false; protected final void register() { @@ -111,6 +116,26 @@ protected final void register() { } } + /// Returns whether a change to the given field should be saved immediately. + /// + /// @param observable the changed observable field + /// @return whether the change should trigger listeners immediately + public boolean shouldSaveImmediately(Observable observable) { + return true; + } + + /// Returns whether a deferred field change still needs to be saved. + public final boolean isSavePending() { + return savePending; + } + + /// Sets whether a deferred field change still needs to be saved. + /// + /// @param savePending whether a deferred field change still needs to be saved + public final void setSavePending(boolean savePending) { + this.savePending = savePending; + } + @Override public void addListener(InvalidationListener listener) { helper.addListener(listener); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java index 7fe40c56323..6c5e6a496d2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java @@ -19,6 +19,7 @@ import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import org.jetbrains.annotations.NotNullByDefault; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -28,6 +29,7 @@ * * @author yushijinhun */ +@NotNullByDefault public final class ObservableHelper implements Observable, InvalidationListener { private final List listeners = new CopyOnWriteArrayList<>(); @@ -63,7 +65,7 @@ public void invalidate() { @Override public void invalidated(Observable observable) { - this.invalidate(); + listeners.forEach(it -> it.invalidated(observable)); } public void receiveUpdatesFrom(Observable observable) {