From 12acecc68fd8c9ad9972f0bb4dc92e764c8eb6c5 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 22:04:15 +0800 Subject: [PATCH 01/17] Add fxsvgimage --- HMCL/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 156969060b..b0860c1234 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(project(":HMCLBoot")) implementation("libs:JFoenix") implementation(libs.twelvemonkeys.imageio.webp) + implementation(libs.fxsvgimage) implementation(libs.java.info) implementation(libs.monet.fx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2897211f61..5ecda420d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ nanohttpd = "2.3.1" jsoup = "1.21.2" chardet = "2.5.0" twelvemonkeys = "3.13.0" +fxsvgimage = "1.4" jna = "5.18.1" pci-ids = "0.4.0" java-info = "1.0" @@ -43,6 +44,7 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } chardet = { module = "org.glavo:chardet", version.ref = "chardet" } twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" } +fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" } jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" } pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" } From 20e8971819a25f01974876da358000f5f21556af Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 22:22:06 +0800 Subject: [PATCH 02/17] update --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 4 +- .../jackhuang/hmcl/ui/image/ImageUtils.java | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index d233b8f584..beaa9351a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -235,8 +235,8 @@ private FXUtils() { public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace"; - public static final List IMAGE_EXTENSIONS = Lang.immutableListOf( - "png", "jpg", "jpeg", "bmp", "gif", "webp", "apng" + public static final List IMAGE_EXTENSIONS = List.of( + "png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng" ); private static final Map builtinImageCache = new ConcurrentHashMap<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index 261ab519cb..ae838497f1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -22,6 +22,11 @@ import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; +import org.girod.javafx.svgimage.LoaderParameters; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; +import org.girod.javafx.svgimage.ScaleQuality; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.image.apng.Png; import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap; import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence; @@ -31,6 +36,7 @@ import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException; import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl; import org.jackhuang.hmcl.util.SwingFXUtils; +import org.jackhuang.hmcl.util.io.IOUtils; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; @@ -39,7 +45,9 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -73,6 +81,33 @@ public final class ImageUtils { return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth); }; + public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> { + String content = IOUtils.readFullyAsString(input); + + LoaderParameters parameters = new LoaderParameters(); + parameters.autoStartAnimations = false; + + // TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread. + SVGImage image = CompletableFuture.supplyAsync( + () -> SVGLoader.load(content, parameters), + Schedulers.javafx() + ).get(); + + if (requestedWidth <= 0. || requestedHeight <= 0.) { + return image.toImage(); + } + + double scaleX = requestedWidth / image.getWidth(); + double scaleY = requestedHeight / image.getHeight(); + + if (preserveRatio) { + double scale = Math.min(scaleX, scaleY); + return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scale, scale); + } else { + return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY); + } + }; + public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> { if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true"))) return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth); @@ -136,11 +171,13 @@ public final class ImageUtils { public static final Map EXT_TO_LOADER = Map.of( "webp", WEBP, + "svg", SVG, "apng", APNG ); public static final Map CONTENT_TYPE_TO_LOADER = Map.of( "image/webp", WEBP, + "image/svg+xml", SVG, "image/apng", APNG ); @@ -165,6 +202,14 @@ public static boolean isWebP(byte[] headerBuffer) { && Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4); } + private static final byte[] SVG_HEADER = " SVG_HEADER.length + && Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length); + } + private static final byte[] PNG_HEADER = { (byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a, @@ -232,6 +277,8 @@ public static boolean isApng(byte[] headerBuffer) { return WEBP; if (isApng(headerBuffer)) return APNG; + if (isSVG(headerBuffer)) + return SVG; return null; } From 5881060a2e3d2b96ac605407ca6ecb4c9f9ed7a0 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 22:55:19 +0800 Subject: [PATCH 03/17] update --- .../hmcl/game/HMCLGameRepository.java | 53 ++++++++++++------- .../ui/versions/GameAdvancedListItem.java | 4 +- .../jackhuang/hmcl/ui/versions/GameItem.java | 2 +- .../hmcl/ui/versions/VersionSettingsPage.java | 2 +- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 2426a1ced2..d35c49f0e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,7 +20,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableObjectValue; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.event.Event; @@ -33,6 +37,7 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionSetting; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.Lang; @@ -52,6 +57,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -298,9 +304,9 @@ public void deleteIconFile(String id) { } } - public Image getVersionIconImage(String id) { + public ObservableObjectValue getVersionIconImage(String id) { if (id == null || !isLoaded()) - return VersionIconType.DEFAULT.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.DEFAULT.getIcon()); VersionSetting vs = getLocalVersionSettingOrCreate(id); VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT; @@ -309,47 +315,54 @@ public Image getVersionIconImage(String id) { Version version = getVersion(id).resolve(this); Optional iconFile = getVersionIconFile(id); if (iconFile.isPresent()) { - try { - return FXUtils.loadImage(iconFile.get()); - } catch (Exception e) { - LOG.warning("Failed to load version icon of " + id, e); - } + var holder = new SimpleObjectProperty(VersionIconType.DEFAULT.getIcon()); + + CompletableFuture.supplyAsync(Lang.wrap(() -> FXUtils.loadImage(iconFile.get())), Schedulers.io()) + .whenCompleteAsync((result, exception) -> { + if (exception == null) { + if (result != null) + holder.setValue(result); + } else + LOG.warning("Failed to load version icon of " + id, exception); + }, Schedulers.javafx()); + + return holder; } if (LibraryAnalyzer.isModded(this, version)) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC)) - return VersionIconType.FABRIC.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.FABRIC.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) - return VersionIconType.QUILT.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.QUILT.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) - return VersionIconType.LEGACY_FABRIC.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.LEGACY_FABRIC.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE)) - return VersionIconType.NEO_FORGE.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.NEO_FORGE.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) - return VersionIconType.FORGE.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.FORGE.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) - return VersionIconType.CLEANROOM.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.CLEANROOM.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER)) - return VersionIconType.CHICKEN.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.CHICKEN.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) - return VersionIconType.OPTIFINE.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.OPTIFINE.getIcon()); } String gameVersion = getGameVersion(version).orElse(null); if (gameVersion != null) { GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion); if (versionNumber.isAprilFools()) { - return VersionIconType.APRIL_FOOLS.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.APRIL_FOOLS.getIcon()); } else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) { - return VersionIconType.COMMAND.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.COMMAND.getIcon()); } else if (versionNumber instanceof GameVersionNumber.Old) { - return VersionIconType.CRAFT_TABLE.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.CRAFT_TABLE.getIcon()); } } - return VersionIconType.GRASS.getIcon(); + return new SimpleObjectProperty<>(VersionIconType.GRASS.getIcon()); } else { - return iconType.getIcon(); + return new SimpleObjectProperty<>(iconType.getIcon()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java index 111590ace9..2401fd90aa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java @@ -68,10 +68,12 @@ private void loadVersion(String version) { Profiles.getSelectedProfile().getRepository().hasVersion(version)) { setTitle(i18n("version.manage.manage")); setSubtitle(version); - imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); + imageView.imageProperty().bind(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); } else { setTitle(i18n("version.empty")); setSubtitle(i18n("version.empty.add")); + + imageView.imageProperty().unbind(); imageView.setImage(VersionIconType.DEFAULT.getIcon()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java index ea9f48c7b9..1d4315df13 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java @@ -113,7 +113,7 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { }, Schedulers.javafx()); title.set(id); - image.set(profile.getRepository().getVersionIconImage(id)); + image.bind(profile.getRepository().getVersionIconImage(id)); } public ReadOnlyStringProperty titleProperty() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index f0424eba78..9b7e363e62 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -726,7 +726,7 @@ private void loadIcon() { return; } - iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId)); + iconPickerItem.imageProperty().bind(profile.getRepository().getVersionIconImage(versionId)); FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32); } From 221bb1a47db28ed5c052331cfa629e957102b0a0 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 23:01:34 +0800 Subject: [PATCH 04/17] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index ae838497f1..f00957ce0f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -93,6 +93,9 @@ public final class ImageUtils { Schedulers.javafx() ).get(); + if (image == null) + throw new IOException("Failed to load SVG image"); + if (requestedWidth <= 0. || requestedHeight <= 0.) { return image.toImage(); } From c2a57a72ccaffa98f06664db3eb2cffb68d77eac Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 23:02:15 +0800 Subject: [PATCH 05/17] update --- .../main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index d35c49f0e1..35d746bd1f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -304,6 +304,7 @@ public void deleteIconFile(String id) { } } + // TODO: Optimize this method public ObservableObjectValue getVersionIconImage(String id) { if (id == null || !isLoaded()) return new SimpleObjectProperty<>(VersionIconType.DEFAULT.getIcon()); From 8274c0ad2bda554f50a1996023009555e2cd0eb9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Feb 2026 23:10:16 +0800 Subject: [PATCH 06/17] update --- .../main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 35d746bd1f..cd0fe865f3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,11 +20,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableObjectValue; import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.event.Event; From aed7d6f082c17defa726ceda02ea0c6ad1ee1f7a Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:27:35 +0800 Subject: [PATCH 07/17] update --- .../hmcl/game/HMCLGameRepository.java | 37 ++-- .../util/javafx/ConstantObservableValue.java | 159 ++++++++++++++++++ 2 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index cd0fe865f3..bb21c31ed9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -21,7 +21,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableObjectValue; +import javafx.beans.value.ObservableValue; import javafx.scene.image.Image; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.LibraryAnalyzer; @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.javafx.ConstantObservableValue; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemInfo; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -302,10 +303,9 @@ public void deleteIconFile(String id) { } } - // TODO: Optimize this method - public ObservableObjectValue getVersionIconImage(String id) { + public ObservableValue getVersionIconImage(String id) { if (id == null || !isLoaded()) - return new SimpleObjectProperty<>(VersionIconType.DEFAULT.getIcon()); + return ConstantObservableValue.of(VersionIconType.DEFAULT.getIcon()); VersionSetting vs = getLocalVersionSettingOrCreate(id); VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT; @@ -314,8 +314,7 @@ public ObservableObjectValue getVersionIconImage(String id) { Version version = getVersion(id).resolve(this); Optional iconFile = getVersionIconFile(id); if (iconFile.isPresent()) { - var holder = new SimpleObjectProperty(VersionIconType.DEFAULT.getIcon()); - + var holder = new SimpleObjectProperty<>(VersionIconType.DEFAULT.getIcon()); CompletableFuture.supplyAsync(Lang.wrap(() -> FXUtils.loadImage(iconFile.get())), Schedulers.io()) .whenCompleteAsync((result, exception) -> { if (exception == null) { @@ -331,37 +330,37 @@ public ObservableObjectValue getVersionIconImage(String id) { if (LibraryAnalyzer.isModded(this, version)) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC)) - return new SimpleObjectProperty<>(VersionIconType.FABRIC.getIcon()); + return ConstantObservableValue.of(VersionIconType.FABRIC.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) - return new SimpleObjectProperty<>(VersionIconType.QUILT.getIcon()); + return ConstantObservableValue.of(VersionIconType.QUILT.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) - return new SimpleObjectProperty<>(VersionIconType.LEGACY_FABRIC.getIcon()); + return ConstantObservableValue.of(VersionIconType.LEGACY_FABRIC.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE)) - return new SimpleObjectProperty<>(VersionIconType.NEO_FORGE.getIcon()); + return ConstantObservableValue.of(VersionIconType.NEO_FORGE.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) - return new SimpleObjectProperty<>(VersionIconType.FORGE.getIcon()); + return ConstantObservableValue.of(VersionIconType.FORGE.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) - return new SimpleObjectProperty<>(VersionIconType.CLEANROOM.getIcon()); + return ConstantObservableValue.of(VersionIconType.CLEANROOM.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER)) - return new SimpleObjectProperty<>(VersionIconType.CHICKEN.getIcon()); + return ConstantObservableValue.of(VersionIconType.CHICKEN.getIcon()); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) - return new SimpleObjectProperty<>(VersionIconType.OPTIFINE.getIcon()); + return ConstantObservableValue.of(VersionIconType.OPTIFINE.getIcon()); } String gameVersion = getGameVersion(version).orElse(null); if (gameVersion != null) { GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion); if (versionNumber.isAprilFools()) { - return new SimpleObjectProperty<>(VersionIconType.APRIL_FOOLS.getIcon()); + return ConstantObservableValue.of(VersionIconType.APRIL_FOOLS.getIcon()); } else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) { - return new SimpleObjectProperty<>(VersionIconType.COMMAND.getIcon()); + return ConstantObservableValue.of(VersionIconType.COMMAND.getIcon()); } else if (versionNumber instanceof GameVersionNumber.Old) { - return new SimpleObjectProperty<>(VersionIconType.CRAFT_TABLE.getIcon()); + return ConstantObservableValue.of(VersionIconType.CRAFT_TABLE.getIcon()); } } - return new SimpleObjectProperty<>(VersionIconType.GRASS.getIcon()); + return ConstantObservableValue.of(VersionIconType.GRASS.getIcon()); } else { - return new SimpleObjectProperty<>(iconType.getIcon()); + return ConstantObservableValue.of(iconType.getIcon()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java new file mode 100644 index 0000000000..580323df10 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java @@ -0,0 +1,159 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util.javafx; + +import javafx.beans.InvalidationListener; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableObjectValue; +import javafx.beans.value.ObservableValue; + +import java.util.Objects; +import java.util.function.Predicate; + +/// A constant observable value is an observable value that always returns the same value. +/// +/// @author Glavo +public interface ConstantObservableValue extends ObservableValue { + + static ObservableObjectValue of(T value) { + class ConstantObservableObjectValue implements ObservableObjectValue, ConstantObservableValue { + @Override + public T get() { + return value; + } + + @Override + public T getValue() { + return value; + } + + private ListenerHolder holder; + + private ListenerHolder getHolder() { + if (holder == null) + holder = new ListenerHolder<>(); + return holder; + } + + @Override + public void addListener(ChangeListener listener) { + getHolder().addListener(listener); + } + + @Override + public void removeListener(ChangeListener listener) { + getHolder().removeListener(listener); + } + + @Override + public void addListener(InvalidationListener listener) { + getHolder().addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + getHolder().removeListener(listener); + } + } + + return new ConstantObservableObjectValue(); + } + + /// Helper class for managing listeners of a constant observable value. + final class ListenerHolder { + + private ListenerList invalidationListeners; + private ListenerList> changeListeners; + + public void addListener(InvalidationListener listener) { + invalidationListeners = ListenerList.add(invalidationListeners, listener); + } + + public void removeListener(InvalidationListener listener) { + invalidationListeners = ListenerList.remove(invalidationListeners, listener); + } + + public void addListener(ChangeListener listener) { + changeListeners = ListenerList.add(changeListeners, listener); + } + + public void removeListener(ChangeListener listener) { + changeListeners = ListenerList.remove(changeListeners, listener); + } + + /// Holder of listeners. + private static final class ListenerList { + + static ListenerList add(ListenerList list, L listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + + if (list == null) + return new ListenerList<>(listener, null); + + return new ListenerList<>(listener, + removeIf(list, it -> it instanceof WeakListener weakListener && weakListener.wasGarbageCollected())); + } + + static ListenerList remove(ListenerList list, L listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + + if (list == null) + return null; + + return removeIf(list, new Predicate<>() { + boolean first = true; + + @Override + public boolean test(L it) { + if (first && listener.equals(it)) { + first = false; + return true; + } else + return it instanceof WeakListener weakListener && weakListener.wasGarbageCollected(); + } + }); + } + + private static ListenerList removeIf(ListenerList list, Predicate predicate) { + if (list == null) + return null; + + ListenerList current = list; + while (current.next != null) { + ListenerList next = current.next; + + if (predicate.test(next.head)) { + current.next = next.next; + continue; + } + current = next; + } + return list; + } + + private final L head; + private ListenerList next; + + private ListenerList(L head, ListenerList next) { + this.head = head; + this.next = next; + } + } + } +} From 7d75ea7498b6b10ab453e434c7888bd3bd87fd9b Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:31:55 +0800 Subject: [PATCH 08/17] update --- .../org/jackhuang/hmcl/ui/image/ImageUtils.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index f00957ce0f..668ff5b58c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -19,6 +19,7 @@ import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; @@ -87,11 +88,17 @@ public final class ImageUtils { LoaderParameters parameters = new LoaderParameters(); parameters.autoStartAnimations = false; + SVGImage image; + // TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread. - SVGImage image = CompletableFuture.supplyAsync( - () -> SVGLoader.load(content, parameters), - Schedulers.javafx() - ).get(); + if (Platform.isFxApplicationThread()) { + image = SVGLoader.load(content, parameters); + } else { + image = CompletableFuture.supplyAsync( + () -> SVGLoader.load(content, parameters), + Schedulers.javafx() + ).get(); + } if (image == null) throw new IOException("Failed to load SVG image"); From 2277ed5211f2da4dc93d69b7805debfb296f5510 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:36:09 +0800 Subject: [PATCH 09/17] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index 668ff5b58c..dcedfaf58c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -37,7 +37,6 @@ import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException; import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl; import org.jackhuang.hmcl.util.SwingFXUtils; -import org.jackhuang.hmcl.util.io.IOUtils; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; @@ -83,7 +82,7 @@ public final class ImageUtils { }; public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> { - String content = IOUtils.readFullyAsString(input); + String content = new String(input.readAllBytes(), StandardCharsets.UTF_8); LoaderParameters parameters = new LoaderParameters(); parameters.autoStartAnimations = false; From 8cf174ce0a28fce0091719695e8af302acdc1215 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:41:26 +0800 Subject: [PATCH 10/17] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index dcedfaf58c..a085a34dc9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -89,10 +89,10 @@ public final class ImageUtils { SVGImage image; - // TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread. if (Platform.isFxApplicationThread()) { image = SVGLoader.load(content, parameters); } else { + // TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread. image = CompletableFuture.supplyAsync( () -> SVGLoader.load(content, parameters), Schedulers.javafx() From b8b9613e56e56b68de309af908e22f2df79d94ee Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:42:43 +0800 Subject: [PATCH 11/17] update --- .../hmcl/game/HMCLGameRepository.java | 51 +++--- .../ui/versions/GameAdvancedListItem.java | 4 +- .../jackhuang/hmcl/ui/versions/GameItem.java | 2 +- .../hmcl/ui/versions/VersionSettingsPage.java | 2 +- .../util/javafx/ConstantObservableValue.java | 159 ------------------ 5 files changed, 23 insertions(+), 195 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index bb21c31ed9..2426a1ced2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,8 +20,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.scene.image.Image; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.LibraryAnalyzer; @@ -35,14 +33,12 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionSetting; -import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.javafx.ConstantObservableValue; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemInfo; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -56,7 +52,6 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -303,9 +298,9 @@ public void deleteIconFile(String id) { } } - public ObservableValue getVersionIconImage(String id) { + public Image getVersionIconImage(String id) { if (id == null || !isLoaded()) - return ConstantObservableValue.of(VersionIconType.DEFAULT.getIcon()); + return VersionIconType.DEFAULT.getIcon(); VersionSetting vs = getLocalVersionSettingOrCreate(id); VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT; @@ -314,53 +309,47 @@ public ObservableValue getVersionIconImage(String id) { Version version = getVersion(id).resolve(this); Optional iconFile = getVersionIconFile(id); if (iconFile.isPresent()) { - var holder = new SimpleObjectProperty<>(VersionIconType.DEFAULT.getIcon()); - CompletableFuture.supplyAsync(Lang.wrap(() -> FXUtils.loadImage(iconFile.get())), Schedulers.io()) - .whenCompleteAsync((result, exception) -> { - if (exception == null) { - if (result != null) - holder.setValue(result); - } else - LOG.warning("Failed to load version icon of " + id, exception); - }, Schedulers.javafx()); - - return holder; + try { + return FXUtils.loadImage(iconFile.get()); + } catch (Exception e) { + LOG.warning("Failed to load version icon of " + id, e); + } } if (LibraryAnalyzer.isModded(this, version)) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC)) - return ConstantObservableValue.of(VersionIconType.FABRIC.getIcon()); + return VersionIconType.FABRIC.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) - return ConstantObservableValue.of(VersionIconType.QUILT.getIcon()); + return VersionIconType.QUILT.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) - return ConstantObservableValue.of(VersionIconType.LEGACY_FABRIC.getIcon()); + return VersionIconType.LEGACY_FABRIC.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE)) - return ConstantObservableValue.of(VersionIconType.NEO_FORGE.getIcon()); + return VersionIconType.NEO_FORGE.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) - return ConstantObservableValue.of(VersionIconType.FORGE.getIcon()); + return VersionIconType.FORGE.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) - return ConstantObservableValue.of(VersionIconType.CLEANROOM.getIcon()); + return VersionIconType.CLEANROOM.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER)) - return ConstantObservableValue.of(VersionIconType.CHICKEN.getIcon()); + return VersionIconType.CHICKEN.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) - return ConstantObservableValue.of(VersionIconType.OPTIFINE.getIcon()); + return VersionIconType.OPTIFINE.getIcon(); } String gameVersion = getGameVersion(version).orElse(null); if (gameVersion != null) { GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion); if (versionNumber.isAprilFools()) { - return ConstantObservableValue.of(VersionIconType.APRIL_FOOLS.getIcon()); + return VersionIconType.APRIL_FOOLS.getIcon(); } else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) { - return ConstantObservableValue.of(VersionIconType.COMMAND.getIcon()); + return VersionIconType.COMMAND.getIcon(); } else if (versionNumber instanceof GameVersionNumber.Old) { - return ConstantObservableValue.of(VersionIconType.CRAFT_TABLE.getIcon()); + return VersionIconType.CRAFT_TABLE.getIcon(); } } - return ConstantObservableValue.of(VersionIconType.GRASS.getIcon()); + return VersionIconType.GRASS.getIcon(); } else { - return ConstantObservableValue.of(iconType.getIcon()); + return iconType.getIcon(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java index 2401fd90aa..111590ace9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java @@ -68,12 +68,10 @@ private void loadVersion(String version) { Profiles.getSelectedProfile().getRepository().hasVersion(version)) { setTitle(i18n("version.manage.manage")); setSubtitle(version); - imageView.imageProperty().bind(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); + imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); } else { setTitle(i18n("version.empty")); setSubtitle(i18n("version.empty.add")); - - imageView.imageProperty().unbind(); imageView.setImage(VersionIconType.DEFAULT.getIcon()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java index 1d4315df13..ea9f48c7b9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java @@ -113,7 +113,7 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { }, Schedulers.javafx()); title.set(id); - image.bind(profile.getRepository().getVersionIconImage(id)); + image.set(profile.getRepository().getVersionIconImage(id)); } public ReadOnlyStringProperty titleProperty() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 9b7e363e62..f0424eba78 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -726,7 +726,7 @@ private void loadIcon() { return; } - iconPickerItem.imageProperty().bind(profile.getRepository().getVersionIconImage(versionId)); + iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId)); FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java deleted file mode 100644 index 580323df10..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ConstantObservableValue.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.util.javafx; - -import javafx.beans.InvalidationListener; -import javafx.beans.WeakListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableObjectValue; -import javafx.beans.value.ObservableValue; - -import java.util.Objects; -import java.util.function.Predicate; - -/// A constant observable value is an observable value that always returns the same value. -/// -/// @author Glavo -public interface ConstantObservableValue extends ObservableValue { - - static ObservableObjectValue of(T value) { - class ConstantObservableObjectValue implements ObservableObjectValue, ConstantObservableValue { - @Override - public T get() { - return value; - } - - @Override - public T getValue() { - return value; - } - - private ListenerHolder holder; - - private ListenerHolder getHolder() { - if (holder == null) - holder = new ListenerHolder<>(); - return holder; - } - - @Override - public void addListener(ChangeListener listener) { - getHolder().addListener(listener); - } - - @Override - public void removeListener(ChangeListener listener) { - getHolder().removeListener(listener); - } - - @Override - public void addListener(InvalidationListener listener) { - getHolder().addListener(listener); - } - - @Override - public void removeListener(InvalidationListener listener) { - getHolder().removeListener(listener); - } - } - - return new ConstantObservableObjectValue(); - } - - /// Helper class for managing listeners of a constant observable value. - final class ListenerHolder { - - private ListenerList invalidationListeners; - private ListenerList> changeListeners; - - public void addListener(InvalidationListener listener) { - invalidationListeners = ListenerList.add(invalidationListeners, listener); - } - - public void removeListener(InvalidationListener listener) { - invalidationListeners = ListenerList.remove(invalidationListeners, listener); - } - - public void addListener(ChangeListener listener) { - changeListeners = ListenerList.add(changeListeners, listener); - } - - public void removeListener(ChangeListener listener) { - changeListeners = ListenerList.remove(changeListeners, listener); - } - - /// Holder of listeners. - private static final class ListenerList { - - static ListenerList add(ListenerList list, L listener) { - Objects.requireNonNull(listener, "listener cannot be null"); - - if (list == null) - return new ListenerList<>(listener, null); - - return new ListenerList<>(listener, - removeIf(list, it -> it instanceof WeakListener weakListener && weakListener.wasGarbageCollected())); - } - - static ListenerList remove(ListenerList list, L listener) { - Objects.requireNonNull(listener, "listener cannot be null"); - - if (list == null) - return null; - - return removeIf(list, new Predicate<>() { - boolean first = true; - - @Override - public boolean test(L it) { - if (first && listener.equals(it)) { - first = false; - return true; - } else - return it instanceof WeakListener weakListener && weakListener.wasGarbageCollected(); - } - }); - } - - private static ListenerList removeIf(ListenerList list, Predicate predicate) { - if (list == null) - return null; - - ListenerList current = list; - while (current.next != null) { - ListenerList next = current.next; - - if (predicate.test(next.head)) { - current.next = next.next; - continue; - } - current = next; - } - return list; - } - - private final L head; - private ListenerList next; - - private ListenerList(L head, ListenerList next) { - this.head = head; - this.next = next; - } - } - } -} From f8ac390667b8fb6da964d62040cc40d6b4745641 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:56:21 +0800 Subject: [PATCH 12/17] update --- .../org/jackhuang/hmcl/ui/image/ImageUtils.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index a085a34dc9..1e7972da03 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -20,9 +20,11 @@ import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.scene.SnapshotParameters; import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; import org.girod.javafx.svgimage.LoaderParameters; import org.girod.javafx.svgimage.SVGImage; import org.girod.javafx.svgimage.SVGLoader; @@ -81,6 +83,12 @@ public final class ImageUtils { return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth); }; + private static final SnapshotParameters DEFAULT_SVG_SNAPSHOT_PARAMS = new SnapshotParameters(); + + { + DEFAULT_SVG_SNAPSHOT_PARAMS.setFill(Color.TRANSPARENT); + } + public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> { String content = new String(input.readAllBytes(), StandardCharsets.UTF_8); @@ -103,16 +111,17 @@ public final class ImageUtils { throw new IOException("Failed to load SVG image"); if (requestedWidth <= 0. || requestedHeight <= 0.) { - return image.toImage(); + return image.toImage(DEFAULT_SVG_SNAPSHOT_PARAMS); } double scaleX = requestedWidth / image.getWidth(); double scaleY = requestedHeight / image.getHeight(); - if (preserveRatio) { + if (preserveRatio || scaleX == scaleY) { double scale = Math.min(scaleX, scaleY); - return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scale, scale); + return image.scale(scale).toImage(DEFAULT_SVG_SNAPSHOT_PARAMS); } else { + // FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY); } }; From 8afac5f0b93b757c1b5a070fd6e7bebce580e67d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 03:57:06 +0800 Subject: [PATCH 13/17] update --- .../org/jackhuang/hmcl/ui/image/ImageUtils.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index 1e7972da03..ab1b89c045 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -83,12 +83,6 @@ public final class ImageUtils { return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth); }; - private static final SnapshotParameters DEFAULT_SVG_SNAPSHOT_PARAMS = new SnapshotParameters(); - - { - DEFAULT_SVG_SNAPSHOT_PARAMS.setFill(Color.TRANSPARENT); - } - public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> { String content = new String(input.readAllBytes(), StandardCharsets.UTF_8); @@ -110,8 +104,11 @@ public final class ImageUtils { if (image == null) throw new IOException("Failed to load SVG image"); + var snapshotParameters = new SnapshotParameters(); + snapshotParameters.setFill(Color.TRANSPARENT); + if (requestedWidth <= 0. || requestedHeight <= 0.) { - return image.toImage(DEFAULT_SVG_SNAPSHOT_PARAMS); + return image.toImage(snapshotParameters); } double scaleX = requestedWidth / image.getWidth(); @@ -119,7 +116,7 @@ public final class ImageUtils { if (preserveRatio || scaleX == scaleY) { double scale = Math.min(scaleX, scaleY); - return image.scale(scale).toImage(DEFAULT_SVG_SNAPSHOT_PARAMS); + return image.scale(scale).toImage(snapshotParameters); } else { // FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY); From 9c5758c5dae1ff4053a1b20e36b684fb2c39a761 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 04:07:07 +0800 Subject: [PATCH 14/17] update --- .../main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 2426a1ced2..ba0e03a91f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -310,7 +310,7 @@ public Image getVersionIconImage(String id) { Optional iconFile = getVersionIconFile(id); if (iconFile.isPresent()) { try { - return FXUtils.loadImage(iconFile.get()); + return FXUtils.loadImage(iconFile.get(), 64, 64, true, true); } catch (Exception e) { LOG.warning("Failed to load version icon of " + id, e); } From 8118b60906b645424dae49f13bfa5df3ac3784c8 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Feb 2026 20:34:54 +0800 Subject: [PATCH 15/17] update --- HMCL/src/main/resources/assets/about/deps.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HMCL/src/main/resources/assets/about/deps.json b/HMCL/src/main/resources/assets/about/deps.json index c121c5360f..a601603ba0 100644 --- a/HMCL/src/main/resources/assets/about/deps.json +++ b/HMCL/src/main/resources/assets/about/deps.json @@ -69,6 +69,11 @@ "subtitle" : "Copyright (C) 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.", "externalLink" : "https://github.com/aellerton/japng" }, + { + "title" : "fxsvgimage", + "subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.", + "externalLink" : "https://github.com/hervegirod/fxsvgimage" + }, { "title": "Terracotta", "subtitle": "Copyright (C) 2025 Burning_TNT.\nAll rights reserved.", From 5d07ba84a10373abf7f800c8c8160e6bd1b63432 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 14 Feb 2026 20:40:51 +0800 Subject: [PATCH 16/17] update --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ecda420d4..dbb19a9c7d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ nanohttpd = "2.3.1" jsoup = "1.21.2" chardet = "2.5.0" twelvemonkeys = "3.13.0" -fxsvgimage = "1.4" +fxsvgimage = "1.3" jna = "5.18.1" pci-ids = "0.4.0" java-info = "1.0" From be6933c88e105cd85c689bf898094c8e3173f366 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Feb 2026 02:59:17 +0800 Subject: [PATCH 17/17] update --- .../src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java index ab1b89c045..038ad71212 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java @@ -111,8 +111,8 @@ public final class ImageUtils { return image.toImage(snapshotParameters); } - double scaleX = requestedWidth / image.getWidth(); - double scaleY = requestedHeight / image.getHeight(); + double scaleX = requestedWidth / image.getScaledWidth(); + double scaleY = requestedHeight / image.getScaledHeight(); if (preserveRatio || scaleX == scaleY) { double scale = Math.min(scaleX, scaleY);