From 886a25a935339971235c19f89720c19c9c523c79 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Wed, 9 Sep 2026 21:00:08 +0800 Subject: [PATCH] =?UTF-8?q?[Enhancement]=20=E5=BB=B6=E5=90=8E=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=B9=B6=E7=BC=93=E5=AD=98=E8=B5=84=E6=BA=90=E5=8C=85?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=20(#6730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/DataPackListPageSkin.java | 49 ++----- .../hmcl/ui/instances/ModListPage.java | 45 ++---- .../ui/instances/ResourcePackListPage.java | 32 +++-- .../addon/resourcepack/ResourcePackFile.java | 8 +- .../resourcepack/ResourcePackFolder.java | 35 +++-- .../resourcepack/ResourcePackManager.java | 8 +- .../resourcepack/ResourcePackZipFile.java | 45 +++--- .../util/javafx/ItemPropertyAsyncCache.java | 133 ++++++++++++++++++ 8 files changed, 219 insertions(+), 136 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ItemPropertyAsyncCache.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java index e2297888d6..ae33941703 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java @@ -27,7 +27,6 @@ import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ListChangeListener; import javafx.collections.transformation.FilteredList; @@ -46,7 +45,6 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.util.Duration; import org.jackhuang.hmcl.addon.datapack.DataPack; -import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -55,15 +53,13 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; -import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.IntStream; @@ -263,11 +259,13 @@ final class DataPackListPageSkin extends SkinBase { private final BooleanProperty activeProperty; private final DataPack.Pack packInfo; - private SoftReference> iconCache; + private final ItemPropertyAsyncCache iconCache; DataPackInfoObject(DataPack.Pack packInfo) { this.packInfo = packInfo; this.activeProperty = packInfo.activeProperty(); + + this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); } String getTitle() { @@ -282,7 +280,11 @@ final class DataPackListPageSkin extends SkinBase { return packInfo; } - Image loadIcon() { + private Image getDefaultIcon() { + return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + } + + private Image loadIcon() { Image image = null; Path imagePath; if (this.getPackInfo().isDirectory()) { @@ -291,7 +293,6 @@ final class DataPackListPageSkin extends SkinBase { image = FXUtils.loadImage(imagePath, 64, 64, true, true); } catch (Exception e) { LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e); - return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); } } else { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(getPackInfo().getPath())) { @@ -301,7 +302,6 @@ final class DataPackListPageSkin extends SkinBase { } } catch (Exception e) { LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e); - return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); } } @@ -309,36 +309,9 @@ final class DataPackListPageSkin extends SkinBase { Math.abs(image.getWidth() - image.getHeight()) < 1) { return image; } else { - return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + return getDefaultIcon(); } } - - public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference> current) { - SoftReference> iconCache = this.iconCache; - CompletableFuture imageFuture; - if (iconCache != null && (imageFuture = iconCache.get()) != null) { - Image image = imageFuture.getNow(null); - if (image != null) { - imageContainer.setImage(image); - return; - } - } else { - imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io()); - this.iconCache = new SoftReference<>(imageFuture); - } - imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png")); - imageFuture.thenAcceptAsync(image -> { - if (current != null) { - ObjectProperty infoObjectProperty = current.get(); - if (infoObjectProperty == null || infoObjectProperty.get() != this) { - // The current ListCell has already switched to another object - return; - } - } - imageContainer.setImage(image); - }, Schedulers.javafx()); - - } } private final class DataPackInfoListCell extends MDListCell { @@ -377,7 +350,7 @@ final class DataPackListPageSkin extends SkinBase { checkBox.selectedProperty().unbindBidirectional(booleanProperty); } checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty); - dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty())); + dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty())); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java index ec759158a2..8de0d08066 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java @@ -23,7 +23,6 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; @@ -64,13 +63,13 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.UncheckedIOException; -import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -599,13 +598,15 @@ public final class ModListPage extends ListPageBase i private final LocalModFile localModFile; private final @Nullable ModTranslations.Mod modTranslations; - private SoftReference> iconCache; + private final ItemPropertyAsyncCache iconCache; ModInfoObject(LocalModFile localModFile) { this.localModFile = localModFile; this.active = localModFile.activeProperty(); this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName()); + + this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); } public LocalModFile getModInfo() { @@ -616,7 +617,10 @@ public final class ModListPage extends ListPageBase i return modTranslations; } - @FXThread + private Image getDefaultIcon() { + return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon(); + } + private Image loadIcon() { List iconPaths = new ArrayList<>(); @@ -639,34 +643,7 @@ public final class ModListPage extends ListPageBase i LOG.warning("Failed to load mod icons", e); } - return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon(); - } - - public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference> current) { - SoftReference> iconCache = this.iconCache; - CompletableFuture imageFuture; - if (iconCache != null && (imageFuture = iconCache.get()) != null) { - Image image = imageFuture.getNow(null); - if (image != null) { - imageContainer.setImage(image); - return; - } - } else { - imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io()); - this.iconCache = new SoftReference<>(imageFuture); - } - imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon()); - imageFuture.thenAcceptAsync(image -> { - if (current != null) { - ObjectProperty infoObjectProperty = current.get(); - if (infoObjectProperty == null || infoObjectProperty.get() != this) { - // The current ListCell has already switched to another object - return; - } - } - - imageContainer.setImage(image); - }, Schedulers.javafx()); + return getDefaultIcon(); } } @@ -683,7 +660,7 @@ public final class ModListPage extends ListPageBase i var imageContainer = new ImageContainer(40); titleContainer.setAlignment(Pos.CENTER_LEFT); - modInfo.loadIcon(imageContainer, null); + modInfo.iconCache.attachValue(imageContainer.imageProperty(), null); TwoLineListItem title = new TwoLineListItem(); title.getTitleLabel().setWrapText(true); @@ -845,7 +822,7 @@ public final class ModListPage extends ListPageBase i ModLoaderType modLoaderType = modInfo.getModLoaderType(); - dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty())); + dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty())); String displayName = modInfo.getName(); if (modTranslations != null && I18n.isUseChinese()) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java index 115fff37cb..8440abc7e0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java @@ -57,14 +57,14 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.*; +import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.lang.ref.WeakReference; import java.nio.file.Path; import java.util.*; import java.util.Objects; @@ -494,13 +494,18 @@ public final class ResourcePackListPage extends ListPageBase iconCache; + public ResourcePackInfoObject(Pair pair) { this.file = pair.key(); this.enabled = new SimpleBooleanProperty(this, "enabled", pair.value()); + + this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); } public ResourcePackFile getFile() { @@ -511,13 +516,16 @@ public final class ResourcePackListPage extends ListPageBase= 1)) { - image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + private Image getDefaultIcon() { + return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + } + + private Image loadIcon() { + Image icon = file.loadIcon(); + if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) { + return icon; } - return image; + return getDefaultIcon(); } } @@ -583,7 +591,7 @@ public final class ResourcePackListPage extends ListPageBase(itemProperty())); content.getTags().clear(); content.setTitle(file.getFileName()); @@ -623,7 +631,7 @@ public final class ResourcePackListPage extends ListPageBase versionRange; if (requiredMajor > 64) - versionRange = getResourcePackVersionRangeNew(resourcePack.getMeta().pack()); + versionRange = getResourcePackVersionRangeNew(resourcePack.getPackInfo()); else if (requiredMajor > 15) - versionRange = getResourcePackVersionRangeOld(resourcePack.getMeta().pack()); + versionRange = getResourcePackVersionRangeOld(resourcePack.getPackInfo()); else - versionRange = getResourcePackVersionRangeOldest(resourcePack.getMeta().pack()); + versionRange = getResourcePackVersionRangeOldest(resourcePack.getPackInfo()); if (versionRange.isEmpty()) return ResourcePackFile.Compatibility.INVALID; if (versionRange.getMaximum().compareTo(this.getRequiredVersion()) < 0) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java index 87aea908cc..4b900350e1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java @@ -36,52 +36,47 @@ import java.util.Optional; import static org.jackhuang.hmcl.util.logging.Logger.LOG; final class ResourcePackZipFile extends ResourcePackFile { - private final PackMcMeta meta; - private final @Nullable Image icon; + private final PackMcMeta.PackInfo info; public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException { super(manager, path); - PackMcMeta metaTemp = null; - byte[] iconData = null; - + PackMcMeta meta = null; try (var zipFileTree = CompressingUtils.openZipTree(path)) { try { - metaTemp = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); + meta = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); } catch (Exception e) { LOG.warning("Failed to parse resource pack meta", e); } + } + this.info = meta != null ? meta.pack() : null; + } + @Override + public PackMcMeta.PackInfo getPackInfo() { + return info; + } + + @Override + public @Nullable Image loadIcon() { + byte[] iconData = null; + try (var zipFileTree = CompressingUtils.openZipTree(getFile())) { var iconEntry = zipFileTree.getEntry("/pack.png"); if (iconEntry != null) { - try { - iconData = zipFileTree.readBinaryEntry(iconEntry); - } catch (Exception e) { - LOG.warning("Failed to load resource pack icon", e); - } + iconData = zipFileTree.readBinaryEntry(iconEntry); } + } catch (Exception e) { + LOG.warning("Failed to load resource pack icon", e); } - this.meta = metaTemp; - Image iconTemp = null; if (iconData != null) { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { - iconTemp = new Image(inputStream, 64, 64, true, true); + return new Image(inputStream, 64, 64, true, true); } catch (Exception e) { LOG.warning("Failed to load resource pack icon", e); } } - this.icon = iconTemp; - } - - @Override - public PackMcMeta getMeta() { - return meta; - } - - @Override - public @Nullable Image getIcon() { - return icon; + return null; } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ItemPropertyAsyncCache.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ItemPropertyAsyncCache.java new file mode 100644 index 0000000000..7ee836abaa --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ItemPropertyAsyncCache.java @@ -0,0 +1,133 @@ +/* + * 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.property.ObjectProperty; +import org.jackhuang.hmcl.task.Schedulers; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/// Cache for a property of an item of [javafx.scene.control.ListCell]. +/// +/// @param type of the property value +/// @param type of the bean, aka. the item +public abstract class ItemPropertyAsyncCache { + + private final B bean; + + public ItemPropertyAsyncCache(B bean) { + this.bean = Objects.requireNonNull(bean); + } + + protected abstract @Nullable T getValue(); + + protected abstract @Nullable T getDefault(); + + /// @return the completable future that loads the value, or null if not set or already garbage-collected + @Nullable + protected abstract CompletableFuture getFuture(); + + protected abstract void setFuture(@NotNull CompletableFuture imageFuture); + + public final void attachValue(ObjectProperty property, @Nullable WeakReference> current) { + CompletableFuture future = getFuture(); + if (future != null) { + T value = future.getNow(null); + if (value != null) { + property.set(value); + return; + } + } else { + future = CompletableFuture.supplyAsync(this::getValue, Schedulers.io()); + setFuture(future); + } + property.set(getDefault()); + future.thenAcceptAsync(image -> { + if (current != null) { + ObjectProperty thisProperty = current.get(); + if (thisProperty == null || thisProperty.get() != bean) { + // The current ListCell has already switched to another object + return; + } + } + + property.set(image); + }, Schedulers.javafx()); + } + + /// + /// @param {@inheritDoc} + /// @param {@inheritDoc} + private static abstract class Base extends ItemPropertyAsyncCache { + + private final Supplier valueSupplier, defaultSupplier; + + /// + /// @param bean the item of list cell + /// @param valueSupplier supplier of the value + /// @param defaultSupplier supplier of the placeholder value + public Base(B bean, Supplier valueSupplier, @Nullable Supplier defaultSupplier) { + super(bean); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + this.defaultSupplier = Objects.requireNonNullElse(defaultSupplier, () -> null); + } + + @Override + protected @Nullable T getValue() { + return valueSupplier.get(); + } + + @Override + protected @Nullable T getDefault() { + return defaultSupplier.get(); + } + } + + /// Implementation of [ItemPropertyAsyncCache], using a [SoftReference] to balance the cost of time and memory. + /// + /// @param {@inheritDoc} + /// @param {@inheritDoc} + public static final class Soft extends Base { + + private SoftReference<@Nullable CompletableFuture> cache = null; + + /// + /// @param bean the item of list cell + /// @param valueSupplier supplier of the value + /// @param defaultSupplier supplier of the placeholder value + public Soft(B bean, Supplier valueSupplier, @Nullable Supplier defaultSupplier) { + super(bean, valueSupplier, defaultSupplier); + } + + @Override + protected @Nullable CompletableFuture getFuture() { + return cache != null ? cache.get() : null; + } + + @Override + protected void setFuture(@NotNull CompletableFuture future) { + this.cache = new SoftReference<>(future); + } + } +}