From 99a45d1314d8ad758d5eaa712fd25809c280242f Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 8 Jan 2026 21:42:30 +0800 Subject: [PATCH 01/24] Create GameItem2 --- .../jackhuang/hmcl/ui/main/GameListPopup.java | 44 +++++++ .../jackhuang/hmcl/ui/versions/GameItem2.java | 121 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java new file mode 100644 index 0000000000..3715ab9531 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java @@ -0,0 +1,44 @@ +/* + * 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.ui.main; + +import com.jfoenix.controls.JFXListCell; +import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; +import javafx.collections.ObservableList; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.Profile; + +import java.util.List; + +public class GameListPopup extends StackPane { + public GameListPopup(Profile profile, ObservableList instances) { + var list = new JFXListView(); + Bindings.bindContent(list.getItems(), instances); + + list.setCellFactory(listView -> new JFXListCell<>() { + }); + + this.getChildren().add(list); + } + + private static final class Cell extends JFXListCell { + + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java new file mode 100644 index 0000000000..02bc00dcda --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java @@ -0,0 +1,121 @@ +/* + * 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.ui.versions; + +import com.google.gson.JsonParseException; +import javafx.application.Platform; +import javafx.beans.property.*; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.mod.ModpackConfiguration; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.util.i18n.I18n; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; +import static org.jackhuang.hmcl.util.Lang.handleUncaught; +import static org.jackhuang.hmcl.util.Lang.threadPool; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class GameItem2 { + private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS); + + private final Profile profile; + private final String id; + + private boolean initialized = false; + private StringProperty title; + private StringProperty tag; + private StringProperty subtitle; + private ObjectProperty image; + + public GameItem2(Profile profile, String id) { + this.profile = profile; + this.id = id; + } + + private void init() { + if (initialized) + return; + + initialized = true; + title = new SimpleStringProperty(); + tag = new SimpleStringProperty(); + subtitle = new SimpleStringProperty(); + image = new SimpleObjectProperty<>(); + + // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) + CompletableFuture.supplyAsync(() -> profile.getRepository().getGameVersion(id), POOL_VERSION_RESOLVE) + .thenAcceptAsync(game -> { + StringBuilder libraries = new StringBuilder(game.orElse(i18n("message.unknown"))); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), game.orElse(null)); + for (LibraryAnalyzer.LibraryMark mark : analyzer) { + String libraryId = mark.getLibraryId(); + String libraryVersion = mark.getLibraryVersion(); + if (libraryId.equals(MINECRAFT.getPatchId())) continue; + if (I18n.hasKey("install.installer." + libraryId)) { + libraries.append(", ").append(i18n("install.installer." + libraryId)); + if (libraryVersion != null) + libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); + } + } + + subtitle.set(libraries.toString()); + }, Platform::runLater) + .exceptionally(handleUncaught); + + CompletableFuture.runAsync(() -> { + try { + ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); + if (config == null) return; + tag.set(config.getVersion()); + } catch (IOException | JsonParseException e) { + LOG.warning("Failed to read modpack configuration from " + id, e); + } + }, Platform::runLater) + .exceptionally(handleUncaught); + + title.set(id); + image.set(profile.getRepository().getVersionIconImage(id)); + } + + public ReadOnlyStringProperty titleProperty() { + init(); + return title; + } + + public ReadOnlyStringProperty tagProperty() { + init(); + return tag; + } + + public ReadOnlyStringProperty subtitleProperty() { + init(); + return subtitle; + } + + public ReadOnlyObjectProperty imageProperty() { + init(); + return image; + } +} From 8381e11530f488e35a3c9c3944b24a659d4c4b52 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 8 Jan 2026 21:57:44 +0800 Subject: [PATCH 02/24] update --- .../jackhuang/hmcl/ui/versions/GameItem2.java | 74 +++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java index 02bc00dcda..3bf8e2c652 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java @@ -17,29 +17,28 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.google.gson.JsonParseException; -import javafx.application.Platform; import javafx.beans.property.*; import javafx.scene.image.Image; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.i18n.I18n; +import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; -import static org.jackhuang.hmcl.util.Lang.handleUncaught; import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class GameItem2 { - private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS); - private final Profile profile; private final String id; @@ -64,36 +63,53 @@ private void init() { subtitle = new SimpleStringProperty(); image = new SimpleObjectProperty<>(); - // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) - CompletableFuture.supplyAsync(() -> profile.getRepository().getGameVersion(id), POOL_VERSION_RESOLVE) - .thenAcceptAsync(game -> { - StringBuilder libraries = new StringBuilder(game.orElse(i18n("message.unknown"))); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), game.orElse(null)); - for (LibraryAnalyzer.LibraryMark mark : analyzer) { - String libraryId = mark.getLibraryId(); - String libraryVersion = mark.getLibraryVersion(); - if (libraryId.equals(MINECRAFT.getPatchId())) continue; - if (I18n.hasKey("install.installer." + libraryId)) { - libraries.append(", ").append(i18n("install.installer." + libraryId)); - if (libraryVersion != null) - libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); - } - } + record Result(@Nullable String gameVersion, @Nullable String tag) { + } - subtitle.set(libraries.toString()); - }, Platform::runLater) - .exceptionally(handleUncaught); + CompletableFuture.supplyAsync(() -> { + // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) + Optional gameVersion = profile.getRepository().getGameVersion(id); - CompletableFuture.runAsync(() -> { + String modPackVersion = null; try { ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); - if (config == null) return; - tag.set(config.getVersion()); - } catch (IOException | JsonParseException e) { + modPackVersion = config != null ? config.getVersion() : null; + } catch (IOException e) { LOG.warning("Failed to read modpack configuration from " + id, e); } - }, Platform::runLater) - .exceptionally(handleUncaught); + + return new Result( + gameVersion.orElse(null), + modPackVersion + ); + }, Schedulers.io()) + .whenCompleteAsync((result, exception) -> { + if (exception == null) { + if (result.gameVersion != null) { + title.set(result.gameVersion); + } + if (result.tag != null) { + tag.set(result.tag); + } + + StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion); + for (LibraryAnalyzer.LibraryMark mark : analyzer) { + String libraryId = mark.getLibraryId(); + String libraryVersion = mark.getLibraryVersion(); + if (libraryId.equals(MINECRAFT.getPatchId())) continue; + if (I18n.hasKey("install.installer." + libraryId)) { + libraries.append(", ").append(i18n("install.installer." + libraryId)); + if (libraryVersion != null) + libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); + } + } + + subtitle.set(libraries.toString()); + } else { + LOG.warning("Failed to read version info from " + id, exception); + } + }, Schedulers.javafx()); title.set(id); image.set(profile.getRepository().getVersionIconImage(id)); From 80f91b6a7face322e455552439bfd07ab0f54b5a Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 8 Jan 2026 22:34:36 +0800 Subject: [PATCH 03/24] update --- .../jackhuang/hmcl/ui/versions/GameItem2.java | 3 - .../hmcl/ui/versions/GameListCell.java | 182 ++++++++++++++++++ .../hmcl/ui/versions/GameListItem.java | 6 + 3 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java index 3bf8e2c652..29f314a8a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java @@ -30,11 +30,8 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; -import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java new file mode 100644 index 0000000000..0e11c1c7d1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -0,0 +1,182 @@ +/* + * 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.ui.versions; + +import com.jfoenix.controls.*; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.ListView; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.IconedMenuItem; +import org.jackhuang.hmcl.ui.construct.MenuSeparator; +import org.jackhuang.hmcl.ui.construct.PopupMenu; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; + +import java.util.function.Consumer; + +import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public final class GameListCell extends JFXListCell { + + private final Region graphic; + + private final JFXRadioButton chkSelected; + private final JFXButton btnUpgrade; + private final JFXButton btnLaunch; + private final JFXButton btnManage; + + private final HBox right; + + public GameListCell() { + BorderPane root = new BorderPane(); + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8, 8, 8, 0)); + + RipplerContainer container = new RipplerContainer(root); + this.graphic = container; + + { + this.chkSelected = new JFXRadioButton(); + root.setLeft(chkSelected); + + BorderPane.setAlignment(chkSelected, Pos.CENTER); + } + +// GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion()); +// gameItem.setMouseTransparent(true); +// root.setCenter(gameItem); + + { + this.right = new HBox(); + root.setRight(right); + + right.setAlignment(Pos.CENTER_RIGHT); + + this.btnUpgrade = new JFXButton(); + btnUpgrade.setOnAction(e -> { + GameListItem item = this.getItem(); + if (item != null) + item.update(); + }); + btnUpgrade.getStyleClass().add("toggle-icon4"); + btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24)); + FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")); + right.getChildren().add(btnUpgrade); + + this.btnLaunch = new JFXButton(); + btnLaunch.setOnAction(e -> { + GameListItem item = this.getItem(); + if (item != null) + item.testGame(); + }); + btnLaunch.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnLaunch, Pos.CENTER); + btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24)); + FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test")); + right.getChildren().add(btnLaunch); + + this.btnManage = new JFXButton(); + btnManage.setOnAction(e -> { + GameListItem item = this.getItem(); + if (item == null) + return; + + preparePopupMenu(item); + + JFXPopup popup = getPopup(getListView()); + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); + popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); + }); + btnManage.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnManage, Pos.CENTER); + btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24)); + FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")); + right.getChildren().add(btnManage); + } + + root.setCursor(Cursor.HAND); + container.setOnMouseClicked(e -> { + GameListItem item = getItem(); + if (item == null) + return; + + if (e.getButton() == MouseButton.PRIMARY) { + if (e.getClickCount() == 1) { + item.modifyGameSettings(); + } + } else if (e.getButton() == MouseButton.SECONDARY) { + preparePopupMenu(item); + + JFXPopup popup = getPopup(getListView()); + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); + popup.show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); + } + }); + } + + @Override + public void updateItem(GameListItem item, boolean empty) { + super.updateItem(item, empty); + } + + // Popup Menu + + private static final String POPUP_ITEM_KEY = GameListCell.class.getName() + ".popup.item"; + + private void preparePopupMenu(GameListItem item) { + this.getListView().getProperties().put(POPUP_ITEM_KEY, item); + } + + private static Runnable getAction(ListView listView, Consumer action) { + return () -> { + if (listView.getProperties().get(POPUP_ITEM_KEY) instanceof GameListItem item) { + action.accept(item); + } + }; + } + + private static JFXPopup getPopup(ListView listView) { + return (JFXPopup) listView.getProperties().computeIfAbsent(GameListCell.class.getName() + ".popup", k -> { + PopupMenu menu = new PopupMenu(); + JFXPopup popup = new JFXPopup(menu); + + menu.getContent().setAll( + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), getAction(listView, GameListItem::testGame), popup), + new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), getAction(listView, GameListItem::generateLaunchScript), popup), + new MenuSeparator(), + new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), getAction(listView, GameListItem::modifyGameSettings), popup), + new MenuSeparator(), + new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), getAction(listView, GameListItem::rename), popup), + new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), getAction(listView, GameListItem::duplicate), popup), + new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), getAction(listView, GameListItem::remove), popup), + new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), getAction(listView, GameListItem::export), popup), + new MenuSeparator(), + new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), getAction(listView, GameListItem::browse), popup)); + + popup.setOnHidden(event -> listView.getProperties().remove(POPUP_ITEM_KEY)); + return popup; + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index bd67514519..f4d1ba4045 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -30,12 +30,14 @@ public class GameListItem extends Control { private final boolean isModpack; private final ToggleGroup toggleGroup; private final BooleanProperty selected = new SimpleBooleanProperty(); + private final GameItem2 gameItem; public GameListItem(ToggleGroup toggleGroup, Profile profile, String id) { this.profile = profile; this.version = id; this.toggleGroup = toggleGroup; this.isModpack = profile.getRepository().isModpack(id); + this.gameItem = new GameItem2(profile, id); selected.set(id.equals(profile.getSelectedVersion())); } @@ -61,6 +63,10 @@ public BooleanProperty selectedProperty() { return selected; } + public GameItem2 getGameItem() { + return gameItem; + } + public void checkSelection() { selected.set(version.equals(profile.getSelectedVersion())); } From da6f82da7cdbb5547e157ab45a1462311738ea3a Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 20:13:04 +0800 Subject: [PATCH 04/24] update --- .../hmcl/ui/versions/GameListCell.java | 60 ++++++++++++++++--- .../hmcl/ui/versions/GameListPage.java | 3 +- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 0e11c1c7d1..7af8157cbd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -18,20 +18,23 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.*; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.ListView; +import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.IconedMenuItem; -import org.jackhuang.hmcl.ui.construct.MenuSeparator; -import org.jackhuang.hmcl.ui.construct.PopupMenu; -import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.StringUtils; import java.util.function.Consumer; @@ -42,6 +45,9 @@ public final class GameListCell extends JFXListCell { private final Region graphic; + private final ImageView imageView; + private final TwoLineListItem content; + private final JFXRadioButton chkSelected; private final JFXButton btnUpgrade; private final JFXButton btnLaunch; @@ -49,6 +55,8 @@ public final class GameListCell extends JFXListCell { private final HBox right; + private final StringProperty tag = new SimpleStringProperty(); + public GameListCell() { BorderPane root = new BorderPane(); root.getStyleClass().add("md-list-cell"); @@ -64,9 +72,31 @@ public GameListCell() { BorderPane.setAlignment(chkSelected, Pos.CENTER); } -// GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion()); -// gameItem.setMouseTransparent(true); -// root.setCenter(gameItem); + { + HBox center = new HBox(); + root.setCenter(center); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + this.imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageViewContainer.getChildren().setAll(imageView); + + this.content = new TwoLineListItem(); + BorderPane.setAlignment(content, Pos.CENTER); + + FXUtils.onChangeAndOperate(tag, tag -> { + content.getTags().clear(); + if (StringUtils.isNotBlank(tag)) + content.addTag(tag); + }); + + center.getChildren().setAll(imageView, content); + } { this.right = new HBox(); @@ -139,6 +169,22 @@ public GameListCell() { @Override public void updateItem(GameListItem item, boolean empty) { super.updateItem(item, empty); + + this.imageView.imageProperty().unbind(); + this.content.titleProperty().unbind(); + this.content.subtitleProperty().unbind(); + this.tag.unbind(); + + if (empty || item == null) { + setGraphic(null); + } else { + setGraphic(this.graphic); + + this.imageView.imageProperty().bind(item.getGameItem().imageProperty()); + this.content.titleProperty().bind(item.getGameItem().titleProperty()); + this.content.subtitleProperty().bind(item.getGameItem().subtitleProperty()); + this.tag.bind(item.getGameItem().tagProperty()); + } } // Popup Menu diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 402fb64095..bea4e83797 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -144,7 +144,7 @@ private void loadVersions(Profile profile) { setLoading(false); List children = repository.getDisplayVersions() .map(version -> new GameListItem(toggleGroup, profile, version.getId())) - .collect(Collectors.toList()); + .toList(); itemsProperty().setAll(children); children.forEach(GameListItem::checkSelection); @@ -182,6 +182,7 @@ private class GameListSkin extends ToolbarListPageSkin { public GameListSkin() { super(GameList.this); + } @Override From 23edd96e124f3fc80ad32241e8a0dff932a624f7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 20:25:49 +0800 Subject: [PATCH 05/24] update --- .../hmcl/ui/ToolbarListPageSkin2.java | 70 +++++++++++++++++++ .../hmcl/ui/versions/GameListPage.java | 4 +- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java new file mode 100644 index 0000000000..e857103208 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java @@ -0,0 +1,70 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.ui; + +import com.jfoenix.controls.JFXListView; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.construct.ComponentList; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; + +import java.util.List; + +public abstract class ToolbarListPageSkin2> extends SkinBase

{ + + protected final JFXListView listView; + + public ToolbarListPageSkin2(P skinnable) { + super(skinnable); + + SpinnerPane spinnerPane = new SpinnerPane(); + spinnerPane.loadingProperty().bind(skinnable.loadingProperty()); + spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty()); + spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); + spinnerPane.getStyleClass().add("large-spinner-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + StackPane.setMargin(root, new Insets(10)); + + List toolbarButtons = initializeToolbar(skinnable); + if (!toolbarButtons.isEmpty()) { + HBox toolbar = new HBox(); + toolbar.setAlignment(Pos.CENTER_LEFT); + toolbar.setPickOnBounds(false); + toolbar.getChildren().setAll(toolbarButtons); + root.getContent().add(toolbar); + } + + { + this.listView = new JFXListView<>(); + root.getContent().add(listView); + } + + spinnerPane.setContent(root); + + getChildren().setAll(spinnerPane); + } + + + protected abstract List initializeToolbar(P skinnable); +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index bea4e83797..eb1ea3637e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -178,11 +178,11 @@ protected GameListSkin createDefaultSkin() { return new GameListSkin(); } - private class GameListSkin extends ToolbarListPageSkin { + private class GameListSkin extends ToolbarListPageSkin2 { public GameListSkin() { super(GameList.this); - + this.listView.setCellFactory(listView -> new GameListCell()); } @Override From b4b414620c07f5dfcee6a067147e1dd33499c5d8 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 20:34:32 +0800 Subject: [PATCH 06/24] update --- .../java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java | 6 +++++- .../java/org/jackhuang/hmcl/ui/versions/GameListCell.java | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java index e857103208..03205fe203 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java @@ -18,17 +18,20 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.SkinBase; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import java.util.List; +// TODO: Replace ToolbarListPageSkin with this class gradually public abstract class ToolbarListPageSkin2> extends SkinBase

{ protected final JFXListView listView; @@ -57,6 +60,8 @@ public ToolbarListPageSkin2(P skinnable) { { this.listView = new JFXListView<>(); + ComponentList.setVgrow(listView, Priority.ALWAYS); + Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); root.getContent().add(listView); } @@ -65,6 +70,5 @@ public ToolbarListPageSkin2(P skinnable) { getChildren().setAll(spinnerPane); } - protected abstract List initializeToolbar(P skinnable); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 7af8157cbd..15854d8715 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -24,6 +24,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; @@ -41,7 +42,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class GameListCell extends JFXListCell { +public final class GameListCell extends ListCell { private final Region graphic; From 625350af5e3820a27d6e0ef8afd55937ff59f3db Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 20:54:51 +0800 Subject: [PATCH 07/24] update --- .../jackhuang/hmcl/ui/versions/GameItem2.java | 14 +- .../hmcl/ui/versions/GameListCell.java | 18 ++- .../hmcl/ui/versions/GameListItem.java | 59 ++------ .../hmcl/ui/versions/GameListItemSkin.java | 135 ------------------ .../hmcl/ui/versions/GameListPage.java | 21 +-- 5 files changed, 44 insertions(+), 203 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java index 29f314a8a5..13ca5447d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java @@ -35,9 +35,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class GameItem2 { - private final Profile profile; - private final String id; +public class GameItem2 { + protected final Profile profile; + protected final String id; private boolean initialized = false; private StringProperty title; @@ -50,6 +50,14 @@ public GameItem2(Profile profile, String id) { this.id = id; } + public Profile getProfile() { + return profile; + } + + public String getId() { + return id; + } + private void init() { if (initialized) return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 15854d8715..f154572423 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.*; -import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; @@ -69,7 +68,6 @@ public GameListCell() { { this.chkSelected = new JFXRadioButton(); root.setLeft(chkSelected); - BorderPane.setAlignment(chkSelected, Pos.CENTER); } @@ -175,16 +173,24 @@ public void updateItem(GameListItem item, boolean empty) { this.content.titleProperty().unbind(); this.content.subtitleProperty().unbind(); this.tag.unbind(); + this.right.getChildren().clear(); + this.chkSelected.selectedProperty().unbind(); if (empty || item == null) { setGraphic(null); } else { setGraphic(this.graphic); - this.imageView.imageProperty().bind(item.getGameItem().imageProperty()); - this.content.titleProperty().bind(item.getGameItem().titleProperty()); - this.content.subtitleProperty().bind(item.getGameItem().subtitleProperty()); - this.tag.bind(item.getGameItem().tagProperty()); + this.chkSelected.selectedProperty().bindBidirectional(item.selectedProperty()); + + this.imageView.imageProperty().bind(item.imageProperty()); + this.content.titleProperty().bind(item.titleProperty()); + this.content.subtitleProperty().bind(item.subtitleProperty()); + this.tag.bind(item.tagProperty()); + if (item.canUpdate()) { + this.right.getChildren().add(btnUpgrade); + } + this.right.getChildren().addAll(btnLaunch, btnManage); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index f4d1ba4045..b20fc24c17 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -19,92 +19,61 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.scene.control.Control; -import javafx.scene.control.Skin; -import javafx.scene.control.ToggleGroup; import org.jackhuang.hmcl.setting.Profile; -public class GameListItem extends Control { - private final Profile profile; - private final String version; +public class GameListItem extends GameItem2 { private final boolean isModpack; - private final ToggleGroup toggleGroup; private final BooleanProperty selected = new SimpleBooleanProperty(); - private final GameItem2 gameItem; - public GameListItem(ToggleGroup toggleGroup, Profile profile, String id) { - this.profile = profile; - this.version = id; - this.toggleGroup = toggleGroup; + public GameListItem(Profile profile, String id) { + super(profile, id); this.isModpack = profile.getRepository().isModpack(id); - this.gameItem = new GameItem2(profile, id); selected.set(id.equals(profile.getSelectedVersion())); } - @Override - protected Skin createDefaultSkin() { - return new GameListItemSkin(this); - } - - public ToggleGroup getToggleGroup() { - return toggleGroup; - } - - public Profile getProfile() { - return profile; - } - - public String getVersion() { - return version; - } - public BooleanProperty selectedProperty() { return selected; } - public GameItem2 getGameItem() { - return gameItem; - } - public void checkSelection() { - selected.set(version.equals(profile.getSelectedVersion())); + selected.set(id.equals(profile.getSelectedVersion())); } public void rename() { - Versions.renameVersion(profile, version); + Versions.renameVersion(profile, id); } public void duplicate() { - Versions.duplicateVersion(profile, version); + Versions.duplicateVersion(profile, id); } public void remove() { - Versions.deleteVersion(profile, version); + Versions.deleteVersion(profile, id); } public void export() { - Versions.exportVersion(profile, version); + Versions.exportVersion(profile, id); } public void browse() { - Versions.openFolder(profile, version); + Versions.openFolder(profile, id); } public void testGame() { - Versions.testGame(profile, version); + Versions.testGame(profile, id); } public void launch() { - Versions.launch(profile, version); + Versions.launch(profile, id); } public void modifyGameSettings() { - Versions.modifyGameSettings(profile, version); + Versions.modifyGameSettings(profile, id); } public void generateLaunchScript() { - Versions.generateLaunchScript(profile, version); + Versions.generateLaunchScript(profile, id); } public boolean canUpdate() { @@ -112,6 +81,6 @@ public boolean canUpdate() { } public void update() { - Versions.updateVersion(profile, version); + Versions.updateVersion(profile, id); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java deleted file mode 100644 index 70b6e8c01b..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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.ui.versions; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXPopup; -import com.jfoenix.controls.JFXRadioButton; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.SkinBase; -import javafx.scene.input.MouseButton; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.IconedMenuItem; -import org.jackhuang.hmcl.ui.construct.MenuSeparator; -import org.jackhuang.hmcl.ui.construct.PopupMenu; -import org.jackhuang.hmcl.ui.construct.RipplerContainer; -import org.jackhuang.hmcl.util.Lazy; - -import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class GameListItemSkin extends SkinBase { - private static GameListItem currentSkinnable; - private static final Lazy popup = new Lazy<>(() -> { - PopupMenu menu = new PopupMenu(); - JFXPopup popup = new JFXPopup(menu); - - menu.getContent().setAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.testGame(), popup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> currentSkinnable.generateLaunchScript(), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), () -> currentSkinnable.modifyGameSettings(), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), () -> currentSkinnable.rename(), popup), - new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), () -> currentSkinnable.duplicate(), popup), - new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), () -> currentSkinnable.remove(), popup), - new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), () -> currentSkinnable.export(), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), () -> currentSkinnable.browse(), popup)); - return popup; - }); - - public GameListItemSkin(GameListItem skinnable) { - super(skinnable); - - BorderPane root = new BorderPane(); - - JFXRadioButton chkSelected = new JFXRadioButton(); - BorderPane.setAlignment(chkSelected, Pos.CENTER); - chkSelected.setUserData(skinnable); - chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty()); - chkSelected.setToggleGroup(skinnable.getToggleGroup()); - root.setLeft(chkSelected); - - GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion()); - gameItem.setMouseTransparent(true); - root.setCenter(gameItem); - - HBox right = new HBox(); - right.setAlignment(Pos.CENTER_RIGHT); - if (skinnable.canUpdate()) { - JFXButton btnUpgrade = new JFXButton(); - btnUpgrade.setOnAction(e -> skinnable.update()); - btnUpgrade.getStyleClass().add("toggle-icon4"); - btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24)); - FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")); - right.getChildren().add(btnUpgrade); - } - - { - JFXButton btnLaunch = new JFXButton(); - btnLaunch.setOnAction(e -> skinnable.testGame()); - btnLaunch.getStyleClass().add("toggle-icon4"); - BorderPane.setAlignment(btnLaunch, Pos.CENTER); - btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24)); - FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test")); - right.getChildren().add(btnLaunch); - } - - { - JFXButton btnManage = new JFXButton(); - btnManage.setOnAction(e -> { - currentSkinnable = skinnable; - - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get()); - popup.get().show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); - }); - btnManage.getStyleClass().add("toggle-icon4"); - BorderPane.setAlignment(btnManage, Pos.CENTER); - btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24)); - FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")); - right.getChildren().add(btnManage); - } - - root.setRight(right); - - root.getStyleClass().add("md-list-cell"); - root.setStyle("-fx-padding: 8 8 8 0"); - - RipplerContainer container = new RipplerContainer(root); - getChildren().setAll(container); - - root.setCursor(Cursor.HAND); - container.setOnMouseClicked(e -> { - if (e.getButton() == MouseButton.PRIMARY) { - if (e.getClickCount() == 1) { - skinnable.modifyGameSettings(); - } - } else if (e.getButton() == MouseButton.SECONDARY) { - currentSkinnable = skinnable; - - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get()); - popup.get().show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); - } - }); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index eb1ea3637e..975eb10467 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -23,7 +23,6 @@ import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.control.ScrollPane; -import javafx.scene.control.ToggleGroup; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -40,7 +39,6 @@ import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -53,8 +51,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage private final ObservableList profileListItems; private final ObjectProperty selectedProfile; - private ToggleGroup toggleGroup; - public GameListPage() { profileListItems = MappedObservableList.create(profilesProperty(), profile -> { ProfileListItem item = new ProfileListItem(profile); @@ -124,6 +120,8 @@ public ReadOnlyObjectProperty stateProperty() { } private class GameList extends ListPageBase { + private final ObjectProperty selectedItem = new SimpleObjectProperty<>(); + public GameList() { super(); @@ -136,14 +134,12 @@ private void loadVersions(Profile profile) { setLoading(true); setFailedReason(null); HMCLGameRepository repository = profile.getRepository(); - toggleGroup = new ToggleGroup(); WeakListenerHolder listenerHolder = new WeakListenerHolder(); - toggleGroup.getProperties().put("ReferenceHolder", listenerHolder); runInFX(() -> { if (profile == Profiles.getSelectedProfile()) { setLoading(false); List children = repository.getDisplayVersions() - .map(version -> new GameListItem(toggleGroup, profile, version.getId())) + .map(version -> new GameListItem(profile, version.getId())) .toList(); itemsProperty().setAll(children); children.forEach(GameListItem::checkSelection); @@ -153,18 +149,15 @@ private void loadVersions(Profile profile) { } profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { - FXUtils.checkFxUserThread(); - children.forEach(it -> it.selectedProperty().set(false)); - children.stream() - .filter(it -> it.getVersion().equals(newValue)) - .findFirst() - .ifPresent(it -> it.selectedProperty().set(true)); + for (GameListItem child : children) { + child.selectedProperty().set(child.getId().equals(newValue)); + } })); } toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { if (toggle == null) return; GameListItem model = (GameListItem) toggle.getUserData(); - model.getProfile().setSelectedVersion(model.getVersion()); + model.getProfile().setSelectedVersion(model.getId()); }); }); } From a96c75a84e7696fa777a73ea4095f4ec4f9d7818 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 21:13:59 +0800 Subject: [PATCH 08/24] update --- .../hmcl/ui/versions/GameListCell.java | 3 +- .../hmcl/ui/versions/GameListItem.java | 6 +- .../hmcl/ui/versions/GameListPage.java | 68 +++++++++++-------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index f154572423..a051a1c5c8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -187,9 +187,8 @@ public void updateItem(GameListItem item, boolean empty) { this.content.titleProperty().bind(item.titleProperty()); this.content.subtitleProperty().bind(item.subtitleProperty()); this.tag.bind(item.tagProperty()); - if (item.canUpdate()) { + if (item.canUpdate()) this.right.getChildren().add(btnUpgrade); - } this.right.getChildren().addAll(btnLaunch, btnManage); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index b20fc24c17..357e13f443 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -23,7 +23,7 @@ public class GameListItem extends GameItem2 { private final boolean isModpack; - private final BooleanProperty selected = new SimpleBooleanProperty(); + private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected"); public GameListItem(Profile profile, String id) { super(profile, id); @@ -36,10 +36,6 @@ public BooleanProperty selectedProperty() { return selected; } - public void checkSelection() { - selected.set(id.equals(profile.getSelectedVersion())); - } - public void rename() { Versions.renameVersion(profile, id); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 975eb10467..779de76ca3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -19,6 +19,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Node; @@ -35,6 +36,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.profile.ProfileListItem; import org.jackhuang.hmcl.ui.profile.ProfilePage; +import org.jackhuang.hmcl.util.FXThread; import org.jackhuang.hmcl.util.javafx.MappedObservableList; import java.util.Collections; @@ -119,47 +121,57 @@ public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); } - private class GameList extends ListPageBase { - private final ObjectProperty selectedItem = new SimpleObjectProperty<>(); + private static class GameList extends ListPageBase { + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + private boolean updatingSelection = false; public GameList() { super(); - Profiles.registerVersionsListener(this::loadVersions); + Profiles.registerVersionsListener(profile -> FXUtils.runInFX(() -> loadVersions(profile))); setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); } + @FXThread private void loadVersions(Profile profile) { setLoading(true); setFailedReason(null); - HMCLGameRepository repository = profile.getRepository(); - WeakListenerHolder listenerHolder = new WeakListenerHolder(); - runInFX(() -> { - if (profile == Profiles.getSelectedProfile()) { - setLoading(false); - List children = repository.getDisplayVersions() - .map(version -> new GameListItem(profile, version.getId())) - .toList(); - itemsProperty().setAll(children); - children.forEach(GameListItem::checkSelection); - - if (children.isEmpty()) { - setFailedReason(i18n("version.empty.hint")); - } + if (profile == Profiles.getSelectedProfile()) { + ObservableList children = FXCollections.observableList(profile.getRepository().getDisplayVersions() + .map(version -> new GameListItem(profile, version.getId())) + .toList()); + setItems(children); + setLoading(false); - profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { - for (GameListItem child : children) { - child.selectedProperty().set(child.getId().equals(newValue)); - } - })); - } - toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { - if (toggle == null) return; - GameListItem model = (GameListItem) toggle.getUserData(); - model.getProfile().setSelectedVersion(model.getId()); + ChangeListener selectionListener = listenerHolder.weak((property, a, b) -> { + if (updatingSelection) return; + updatingSelection = true; + + GameListItem item = (GameListItem) ((Property) property).getBean(); + profile.setSelectedVersion(item.getId()); + + updatingSelection = false; }); - }); + + String selectedId = profile.getSelectedVersion(); + for (GameListItem child : children) { + child.selectedProperty().set(child.getId().equals(selectedId)); + child.selectedProperty().addListener(selectionListener); + } + + if (children.isEmpty()) { + setFailedReason(i18n("version.empty.hint")); + } + + profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { + updatingSelection = true; + for (GameListItem child : children) { + child.selectedProperty().set(child.getId().equals(newValue)); + } + updatingSelection = false; + })); + } } public void refreshList() { From 1b6e82bbbc1edf8d8f8f82902512d2985ee1feae Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 21:20:24 +0800 Subject: [PATCH 09/24] update --- .../jackhuang/hmcl/ui/WeakListenerHolder.java | 4 ++ .../hmcl/ui/versions/GameListPage.java | 59 ++++++++++--------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHolder.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHolder.java index 4a7032d38b..c045160743 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHolder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHolder.java @@ -55,4 +55,8 @@ public void add(Object obj) { public boolean remove(Object obj) { return refs.remove(obj); } + + public void clear() { + refs.clear(); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 779de76ca3..7095a845fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -135,43 +135,44 @@ public GameList() { @FXThread private void loadVersions(Profile profile) { + listenerHolder.clear(); setLoading(true); setFailedReason(null); - if (profile == Profiles.getSelectedProfile()) { - ObservableList children = FXCollections.observableList(profile.getRepository().getDisplayVersions() - .map(version -> new GameListItem(profile, version.getId())) - .toList()); - setItems(children); - setLoading(false); + if (profile != Profiles.getSelectedProfile()) + return; + + ObservableList children = FXCollections.observableList(profile.getRepository().getDisplayVersions() + .map(instance -> new GameListItem(profile, instance.getId())) + .toList()); + setItems(children); + if (children.isEmpty()) { + setFailedReason(i18n("version.empty.hint")); + } + setLoading(false); - ChangeListener selectionListener = listenerHolder.weak((property, a, b) -> { - if (updatingSelection) return; - updatingSelection = true; + ChangeListener selectionListener = listenerHolder.weak((property, oldValue, newValue) -> { + if (!newValue || updatingSelection) return; + updatingSelection = true; - GameListItem item = (GameListItem) ((Property) property).getBean(); - profile.setSelectedVersion(item.getId()); + GameListItem item = (GameListItem) ((Property) property).getBean(); + profile.setSelectedVersion(item.getId()); - updatingSelection = false; - }); + updatingSelection = false; + }); - String selectedId = profile.getSelectedVersion(); - for (GameListItem child : children) { - child.selectedProperty().set(child.getId().equals(selectedId)); - child.selectedProperty().addListener(selectionListener); - } + String selectedId = profile.getSelectedVersion(); + for (GameListItem child : children) { + child.selectedProperty().set(child.getId().equals(selectedId)); + child.selectedProperty().addListener(selectionListener); + } - if (children.isEmpty()) { - setFailedReason(i18n("version.empty.hint")); + profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { + updatingSelection = true; + for (GameListItem child : children) { + child.selectedProperty().set(child.getId().equals(newValue)); } - - profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { - updatingSelection = true; - for (GameListItem child : children) { - child.selectedProperty().set(child.getId().equals(newValue)); - } - updatingSelection = false; - })); - } + updatingSelection = false; + })); } public void refreshList() { From a65c108be19bea1732fccdf64f74ceda83f1b7b7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 21:25:09 +0800 Subject: [PATCH 10/24] update --- .../java/org/jackhuang/hmcl/ui/versions/GameListCell.java | 1 - .../java/org/jackhuang/hmcl/ui/versions/GameListPage.java | 5 ----- 2 files changed, 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index a051a1c5c8..c82da55251 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -182,7 +182,6 @@ public void updateItem(GameListItem item, boolean empty) { setGraphic(this.graphic); this.chkSelected.selectedProperty().bindBidirectional(item.selectedProperty()); - this.imageView.imageProperty().bind(item.imageProperty()); this.content.titleProperty().bind(item.titleProperty()); this.content.subtitleProperty().bind(item.subtitleProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 7095a845fd..d0fc371525 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -126,8 +126,6 @@ private static class GameList extends ListPageBase { private boolean updatingSelection = false; public GameList() { - super(); - Profiles.registerVersionsListener(profile -> FXUtils.runInFX(() -> loadVersions(profile))); setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); @@ -152,12 +150,9 @@ private void loadVersions(Profile profile) { ChangeListener selectionListener = listenerHolder.weak((property, oldValue, newValue) -> { if (!newValue || updatingSelection) return; - updatingSelection = true; GameListItem item = (GameListItem) ((Property) property).getBean(); profile.setSelectedVersion(item.getId()); - - updatingSelection = false; }); String selectedId = profile.getSelectedVersion(); From d22169ab144a425234e510c4527d52df2d22ada7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:01:06 +0800 Subject: [PATCH 11/24] update --- .../hmcl/ui/versions/GameListCell.java | 17 +++++++++++++++-- .../hmcl/ui/versions/GameListItem.java | 2 -- .../hmcl/ui/versions/GameListPage.java | 11 ----------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index c82da55251..6ba210cf94 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -20,11 +20,13 @@ import com.jfoenix.controls.*; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.ToggleGroup; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; @@ -66,7 +68,18 @@ public GameListCell() { this.graphic = container; { - this.chkSelected = new JFXRadioButton(); + this.chkSelected = new JFXRadioButton() { + @Override + public void fire() { + if (!isDisable() && !isSelected()) { + fireEvent(new ActionEvent()); + GameListItem item = GameListCell.this.getItem(); + if (item != null) { + item.getProfile().setSelectedVersion(item.getId()); + } + } + } + }; root.setLeft(chkSelected); BorderPane.setAlignment(chkSelected, Pos.CENTER); } @@ -181,7 +194,7 @@ public void updateItem(GameListItem item, boolean empty) { } else { setGraphic(this.graphic); - this.chkSelected.selectedProperty().bindBidirectional(item.selectedProperty()); + this.chkSelected.selectedProperty().bind(item.selectedProperty()); this.imageView.imageProperty().bind(item.imageProperty()); this.content.titleProperty().bind(item.titleProperty()); this.content.subtitleProperty().bind(item.subtitleProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index 357e13f443..d061dd62c5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -28,8 +28,6 @@ public class GameListItem extends GameItem2 { public GameListItem(Profile profile, String id) { super(profile, id); this.isModpack = profile.getRepository().isModpack(id); - - selected.set(id.equals(profile.getSelectedVersion())); } public BooleanProperty selectedProperty() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index d0fc371525..28ffa17368 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -19,14 +19,12 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; -import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.*; @@ -42,7 +40,6 @@ import java.util.Collections; import java.util.List; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; @@ -148,17 +145,9 @@ private void loadVersions(Profile profile) { } setLoading(false); - ChangeListener selectionListener = listenerHolder.weak((property, oldValue, newValue) -> { - if (!newValue || updatingSelection) return; - - GameListItem item = (GameListItem) ((Property) property).getBean(); - profile.setSelectedVersion(item.getId()); - }); - String selectedId = profile.getSelectedVersion(); for (GameListItem child : children) { child.selectedProperty().set(child.getId().equals(selectedId)); - child.selectedProperty().addListener(selectionListener); } profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { From 4014eb768ec88223d693b11bc501e9fc1d3ebb84 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:03:38 +0800 Subject: [PATCH 12/24] update --- .../jackhuang/hmcl/ui/versions/GameListItem.java | 4 +++- .../jackhuang/hmcl/ui/versions/GameListPage.java | 13 ------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index d061dd62c5..885e3e56e0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.jackhuang.hmcl.setting.Profile; @@ -28,9 +29,10 @@ public class GameListItem extends GameItem2 { public GameListItem(Profile profile, String id) { super(profile, id); this.isModpack = profile.getRepository().isModpack(id); + selected.bind(profile.selectedVersionProperty().isEqualTo(id)); } - public BooleanProperty selectedProperty() { + public ReadOnlyBooleanProperty selectedProperty() { return selected; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 28ffa17368..9ae0f13ffc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -144,19 +144,6 @@ private void loadVersions(Profile profile) { setFailedReason(i18n("version.empty.hint")); } setLoading(false); - - String selectedId = profile.getSelectedVersion(); - for (GameListItem child : children) { - child.selectedProperty().set(child.getId().equals(selectedId)); - } - - profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { - updatingSelection = true; - for (GameListItem child : children) { - child.selectedProperty().set(child.getId().equals(newValue)); - } - updatingSelection = false; - })); } public void refreshList() { From 1a50fa3c84170a0652ec25048a94a767227b1663 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:13:57 +0800 Subject: [PATCH 13/24] update --- .../jackhuang/hmcl/ui/versions/GameItem.java | 159 ++++++++++-------- .../jackhuang/hmcl/ui/versions/GameItem2.java | 142 ---------------- .../hmcl/ui/versions/GameItemSkin.java | 59 ------- 3 files changed, 89 insertions(+), 271 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItemSkin.java 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 c34d1ce020..13ca5447d7 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 @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * 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 @@ -17,107 +17,126 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.google.gson.JsonParseException; -import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.control.Control; -import javafx.scene.control.Skin; +import javafx.beans.property.*; import javafx.scene.image.Image; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.i18n.I18n; +import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; -import static org.jackhuang.hmcl.util.Lang.handleUncaught; -import static org.jackhuang.hmcl.util.Lang.threadPool; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class GameItem extends Control { - - private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS); +public class GameItem2 { + protected final Profile profile; + protected final String id; - private final Profile profile; - private final String version; - private final StringProperty title = new SimpleStringProperty(); - private final StringProperty tag = new SimpleStringProperty(); - private final StringProperty subtitle = new SimpleStringProperty(); - private final ObjectProperty image = new SimpleObjectProperty<>(); + private boolean initialized = false; + private StringProperty title; + private StringProperty tag; + private StringProperty subtitle; + private ObjectProperty image; - public GameItem(Profile profile, String id) { + public GameItem2(Profile profile, String id) { this.profile = profile; - this.version = id; - - // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) - CompletableFuture.supplyAsync(() -> profile.getRepository().getGameVersion(id), POOL_VERSION_RESOLVE) - .thenAcceptAsync(game -> { - StringBuilder libraries = new StringBuilder(game.orElse(i18n("message.unknown"))); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), game.orElse(null)); - for (LibraryAnalyzer.LibraryMark mark : analyzer) { - String libraryId = mark.getLibraryId(); - String libraryVersion = mark.getLibraryVersion(); - if (libraryId.equals(MINECRAFT.getPatchId())) continue; - if (I18n.hasKey("install.installer." + libraryId)) { - libraries.append(", ").append(i18n("install.installer." + libraryId)); - if (libraryVersion != null) - libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); - } - } - - subtitle.set(libraries.toString()); - }, Platform::runLater) - .exceptionally(handleUncaught); - - CompletableFuture.runAsync(() -> { - try { - ModpackConfiguration config = profile.getRepository().readModpackConfiguration(version); - if (config == null) return; - tag.set(config.getVersion()); - } catch (IOException | JsonParseException e) { - LOG.warning("Failed to read modpack configuration from " + version, e); - } - }, Platform::runLater) - .exceptionally(handleUncaught); - - title.set(id); - image.set(profile.getRepository().getVersionIconImage(version)); - } - - @Override - protected Skin createDefaultSkin() { - return new GameItemSkin(this); + this.id = id; } public Profile getProfile() { return profile; } - public String getVersion() { - return version; + public String getId() { + return id; + } + + private void init() { + if (initialized) + return; + + initialized = true; + title = new SimpleStringProperty(); + tag = new SimpleStringProperty(); + subtitle = new SimpleStringProperty(); + image = new SimpleObjectProperty<>(); + + record Result(@Nullable String gameVersion, @Nullable String tag) { + } + + CompletableFuture.supplyAsync(() -> { + // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) + Optional gameVersion = profile.getRepository().getGameVersion(id); + + String modPackVersion = null; + try { + ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); + modPackVersion = config != null ? config.getVersion() : null; + } catch (IOException e) { + LOG.warning("Failed to read modpack configuration from " + id, e); + } + + return new Result( + gameVersion.orElse(null), + modPackVersion + ); + }, Schedulers.io()) + .whenCompleteAsync((result, exception) -> { + if (exception == null) { + if (result.gameVersion != null) { + title.set(result.gameVersion); + } + if (result.tag != null) { + tag.set(result.tag); + } + + StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion); + for (LibraryAnalyzer.LibraryMark mark : analyzer) { + String libraryId = mark.getLibraryId(); + String libraryVersion = mark.getLibraryVersion(); + if (libraryId.equals(MINECRAFT.getPatchId())) continue; + if (I18n.hasKey("install.installer." + libraryId)) { + libraries.append(", ").append(i18n("install.installer." + libraryId)); + if (libraryVersion != null) + libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); + } + } + + subtitle.set(libraries.toString()); + } else { + LOG.warning("Failed to read version info from " + id, exception); + } + }, Schedulers.javafx()); + + title.set(id); + image.set(profile.getRepository().getVersionIconImage(id)); } - public StringProperty titleProperty() { + public ReadOnlyStringProperty titleProperty() { + init(); return title; } - public StringProperty tagProperty() { + public ReadOnlyStringProperty tagProperty() { + init(); return tag; } - public StringProperty subtitleProperty() { + public ReadOnlyStringProperty subtitleProperty() { + init(); return subtitle; } - public ObjectProperty imageProperty() { + public ReadOnlyObjectProperty imageProperty() { + init(); return image; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java deleted file mode 100644 index 13ca5447d7..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem2.java +++ /dev/null @@ -1,142 +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.ui.versions; - -import javafx.beans.property.*; -import javafx.scene.image.Image; -import org.jackhuang.hmcl.download.LibraryAnalyzer; -import org.jackhuang.hmcl.mod.ModpackConfiguration; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public class GameItem2 { - protected final Profile profile; - protected final String id; - - private boolean initialized = false; - private StringProperty title; - private StringProperty tag; - private StringProperty subtitle; - private ObjectProperty image; - - public GameItem2(Profile profile, String id) { - this.profile = profile; - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public String getId() { - return id; - } - - private void init() { - if (initialized) - return; - - initialized = true; - title = new SimpleStringProperty(); - tag = new SimpleStringProperty(); - subtitle = new SimpleStringProperty(); - image = new SimpleObjectProperty<>(); - - record Result(@Nullable String gameVersion, @Nullable String tag) { - } - - CompletableFuture.supplyAsync(() -> { - // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) - Optional gameVersion = profile.getRepository().getGameVersion(id); - - String modPackVersion = null; - try { - ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); - modPackVersion = config != null ? config.getVersion() : null; - } catch (IOException e) { - LOG.warning("Failed to read modpack configuration from " + id, e); - } - - return new Result( - gameVersion.orElse(null), - modPackVersion - ); - }, Schedulers.io()) - .whenCompleteAsync((result, exception) -> { - if (exception == null) { - if (result.gameVersion != null) { - title.set(result.gameVersion); - } - if (result.tag != null) { - tag.set(result.tag); - } - - StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion); - for (LibraryAnalyzer.LibraryMark mark : analyzer) { - String libraryId = mark.getLibraryId(); - String libraryVersion = mark.getLibraryVersion(); - if (libraryId.equals(MINECRAFT.getPatchId())) continue; - if (I18n.hasKey("install.installer." + libraryId)) { - libraries.append(", ").append(i18n("install.installer." + libraryId)); - if (libraryVersion != null) - libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); - } - } - - subtitle.set(libraries.toString()); - } else { - LOG.warning("Failed to read version info from " + id, exception); - } - }, Schedulers.javafx()); - - title.set(id); - image.set(profile.getRepository().getVersionIconImage(id)); - } - - public ReadOnlyStringProperty titleProperty() { - init(); - return title; - } - - public ReadOnlyStringProperty tagProperty() { - init(); - return tag; - } - - public ReadOnlyStringProperty subtitleProperty() { - init(); - return subtitle; - } - - public ReadOnlyObjectProperty imageProperty() { - init(); - return image; - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItemSkin.java deleted file mode 100644 index b7b79f6213..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItemSkin.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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.ui.versions; - -import javafx.geometry.Pos; -import javafx.scene.control.SkinBase; -import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.TwoLineListItem; -import org.jackhuang.hmcl.util.StringUtils; - -public class GameItemSkin extends SkinBase { - public GameItemSkin(GameItem skinnable) { - super(skinnable); - - HBox center = new HBox(); - center.setSpacing(8); - center.setAlignment(Pos.CENTER_LEFT); - - StackPane imageViewContainer = new StackPane(); - FXUtils.setLimitWidth(imageViewContainer, 32); - FXUtils.setLimitHeight(imageViewContainer, 32); - - ImageView imageView = new ImageView(); - FXUtils.limitSize(imageView, 32, 32); - imageView.imageProperty().bind(skinnable.imageProperty()); - imageViewContainer.getChildren().setAll(imageView); - - TwoLineListItem item = new TwoLineListItem(); - item.titleProperty().bind(skinnable.titleProperty()); - FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> { - item.getTags().clear(); - if (StringUtils.isNotBlank(tag)) - item.addTag(tag); - }); - item.subtitleProperty().bind(skinnable.subtitleProperty()); - BorderPane.setAlignment(item, Pos.CENTER); - center.getChildren().setAll(imageView, item); - getChildren().setAll(center); - } -} From b56e7fbd6d61cdb10009e338e65abfdd95d2a0e2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:33:21 +0800 Subject: [PATCH 14/24] update --- .../org/jackhuang/hmcl/ui/main/MainPage.java | 42 +----- .../jackhuang/hmcl/ui/versions/GameItem.java | 4 +- .../hmcl/ui/versions/GameListItem.java | 2 +- .../hmcl/ui/versions/GameListPopupMenu.java | 139 ++++++++++++++++++ 4 files changed, 148 insertions(+), 39 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 00aedc61b0..39056251fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -29,8 +30,8 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; @@ -59,10 +60,10 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.versions.GameItem; +import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChecker; @@ -73,7 +74,6 @@ import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.javafx.BindingMapping; -import org.jackhuang.hmcl.util.javafx.MappedObservableList; import java.io.IOException; import java.util.List; @@ -92,16 +92,10 @@ public final class MainPage extends StackPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); - private final PopupMenu menu = new PopupMenu(); - - private final StackPane popupWrapper = new StackPane(menu); - private final JFXPopup popup = new JFXPopup(popupWrapper); - private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame"); private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate"); private final ObjectProperty latestVersion = new SimpleObjectProperty<>(this, "latestVersion"); private final ObservableList versions = FXCollections.observableArrayList(); - private final ObservableList versionNodes; private Profile profile; private TransitionPane announcementPane; @@ -273,19 +267,6 @@ public void accept(String currentGame) { getChildren().addAll(updatePane, launchPane); - menu.setMaxHeight(365); - menu.setMaxWidth(545); - menu.setAlwaysShowingVBar(false); - FXUtils.onClicked(menu, popup::hide); - versionNodes = MappedObservableList.create(versions, version -> { - Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId())); - FXUtils.onClicked(node, () -> { - profile.setSelectedVersion(version.getId()); - popup.hide(); - }); - return node; - }); - Bindings.bindContent(menu.getContent(), versionNodes); } private void showUpdate(boolean show) { @@ -365,20 +346,9 @@ private void launchNoGame() { } private void onMenu() { - Node contentNode; - if (menu.getContent().isEmpty()) { - Label placeholder = new Label(i18n("version.empty")); - placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;"); - contentNode = placeholder; - } else { - contentNode = menu; - } - - popupWrapper.getChildren().setAll(contentNode); - - if (popup.isShowing()) { - popup.hide(); - } + GameListPopupMenu menu = new GameListPopupMenu(); + menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList()); + JFXPopup popup = new JFXPopup(menu); popup.show( menuButton, JFXPopup.PopupVPosition.BOTTOM, 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 13ca5447d7..7f67b8a3e8 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 @@ -35,7 +35,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class GameItem2 { +public class GameItem { protected final Profile profile; protected final String id; @@ -45,7 +45,7 @@ public class GameItem2 { private StringProperty subtitle; private ObjectProperty image; - public GameItem2(Profile profile, String id) { + public GameItem(Profile profile, String id) { this.profile = profile; this.id = id; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java index 885e3e56e0..8151215a5a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -22,7 +22,7 @@ import javafx.beans.property.SimpleBooleanProperty; import org.jackhuang.hmcl.setting.Profile; -public class GameListItem extends GameItem2 { +public class GameListItem extends GameItem { private final boolean isModpack; private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java new file mode 100644 index 0000000000..d74f6fac20 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java @@ -0,0 +1,139 @@ +/* + * 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.ui.versions; + +import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXPopup; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.util.StringUtils; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +/// @author Glavo +public final class GameListPopupMenu extends StackPane { + private final JFXListView listView = new JFXListView<>(); + private final BooleanBinding isEmpty = Bindings.isEmpty(listView.getItems()); + + public GameListPopupMenu() { + this.setMaxHeight(365); + this.setMaxWidth(545); + + listView.setCellFactory(listView -> new Cell()); + + Label placeholder = new Label(i18n("version.empty")); + placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;"); + + FXUtils.onChangeAndOperate(isEmpty, empty -> { + getChildren().setAll(empty ? placeholder : listView); + }); + } + + public ObservableList getItems() { + return listView.getItems(); + } + + private static final class Cell extends ListCell { + + private final Region graphic; + + private final ImageView imageView; + private final TwoLineListItem content; + + private final StringProperty tag = new SimpleStringProperty(); + + public Cell() { + HBox root = new HBox(); + + root.setSpacing(8); + root.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + this.imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + // imageView.imageProperty().bind(skinnable.imageProperty()); + imageViewContainer.getChildren().setAll(imageView); + + this.content = new TwoLineListItem(); + // content.titleProperty().bind(skinnable.titleProperty()); + FXUtils.onChangeAndOperate(tag, tag -> { + content.getTags().clear(); + if (StringUtils.isNotBlank(tag)) { + content.addTag(tag); + } + }); + // content.subtitleProperty().bind(skinnable.subtitleProperty()); + BorderPane.setAlignment(content, Pos.CENTER); + root.getChildren().setAll(imageView, content); + + StackPane pane = new StackPane(); + pane.getChildren().setAll(root); + pane.getStyleClass().add("menu-container"); + root.setMouseTransparent(true); + + RipplerContainer ripplerContainer = new RipplerContainer(pane); + FXUtils.onClicked(ripplerContainer, () -> { + GameItem item = getItem(); + if (item != null) { + item.getProfile().setSelectedVersion(item.getId()); + if (getScene().getWindow() instanceof JFXPopup popup) + popup.hide(); + } + }); + this.graphic = ripplerContainer; + } + + @Override + protected void updateItem(GameItem item, boolean empty) { + super.updateItem(item, empty); + + this.imageView.imageProperty().unbind(); + this.content.titleProperty().unbind(); + this.content.subtitleProperty().unbind(); + this.tag.unbind(); + + if (empty || item == null) { + setGraphic(null); + } else { + setGraphic(this.graphic); + + this.imageView.imageProperty().bind(item.imageProperty()); + this.content.titleProperty().bind(item.titleProperty()); + this.content.subtitleProperty().bind(item.subtitleProperty()); + this.tag.bind(item.tagProperty()); + } + } + } +} From 5dfb2dede28cd705047ced9ca91ebd084f64dd6c Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:33:44 +0800 Subject: [PATCH 15/24] update --- .../java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java index d74f6fac20..eeb59cf231 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java @@ -83,18 +83,15 @@ public Cell() { this.imageView = new ImageView(); FXUtils.limitSize(imageView, 32, 32); - // imageView.imageProperty().bind(skinnable.imageProperty()); imageViewContainer.getChildren().setAll(imageView); this.content = new TwoLineListItem(); - // content.titleProperty().bind(skinnable.titleProperty()); FXUtils.onChangeAndOperate(tag, tag -> { content.getTags().clear(); if (StringUtils.isNotBlank(tag)) { content.addTag(tag); } }); - // content.subtitleProperty().bind(skinnable.subtitleProperty()); BorderPane.setAlignment(content, Pos.CENTER); root.getChildren().setAll(imageView, content); From 3477feea8ac89757160e166e02223f9927f0c3f6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 22:57:06 +0800 Subject: [PATCH 16/24] update --- .../org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java index eeb59cf231..bfbe678dc7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java @@ -24,6 +24,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.ObservableList; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ListCell; @@ -46,9 +47,13 @@ public final class GameListPopupMenu extends StackPane { public GameListPopupMenu() { this.setMaxHeight(365); - this.setMaxWidth(545); + this.getStyleClass().add("popup-menu-content"); listView.setCellFactory(listView -> new Cell()); + listView.setFixedCellSize(60); + listView.setPrefWidth(300); + + listView.prefHeightProperty().bind(Bindings.size(getItems()).multiply(60).add(2)); Label placeholder = new Label(i18n("version.empty")); placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;"); @@ -72,6 +77,7 @@ private static final class Cell extends ListCell { private final StringProperty tag = new SimpleStringProperty(); public Cell() { + this.setPadding(Insets.EMPTY); HBox root = new HBox(); root.setSpacing(8); From f4230a89652d74148d3f50180ecb79afb2c6477e Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 9 Jan 2026 23:01:23 +0800 Subject: [PATCH 17/24] update --- .../jackhuang/hmcl/ui/main/GameListPopup.java | 44 ----------- .../org/jackhuang/hmcl/ui/main/MainPage.java | 3 - .../jackhuang/hmcl/ui/versions/GameItem.java | 76 +++++++++---------- .../hmcl/ui/versions/GameListCell.java | 1 - 4 files changed, 35 insertions(+), 89 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java deleted file mode 100644 index 3715ab9531..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/GameListPopup.java +++ /dev/null @@ -1,44 +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.ui.main; - -import com.jfoenix.controls.JFXListCell; -import com.jfoenix.controls.JFXListView; -import javafx.beans.binding.Bindings; -import javafx.collections.ObservableList; -import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.setting.Profile; - -import java.util.List; - -public class GameListPopup extends StackPane { - public GameListPopup(Profile profile, ObservableList instances) { - var list = new JFXListView(); - Bindings.bindContent(list.getItems(), instances); - - list.setCellFactory(listView -> new JFXListCell<>() { - }); - - this.getChildren().add(list); - } - - private static final class Cell extends JFXListCell { - - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 39056251fd..10ebffdc0a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -18,12 +18,10 @@ package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; -import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -31,7 +29,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; -import javafx.scene.control.ListCell; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; 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 7f67b8a3e8..547d790b66 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 @@ -72,49 +72,43 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { } CompletableFuture.supplyAsync(() -> { - // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) - Optional gameVersion = profile.getRepository().getGameVersion(id); - - String modPackVersion = null; - try { - ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); - modPackVersion = config != null ? config.getVersion() : null; - } catch (IOException e) { - LOG.warning("Failed to read modpack configuration from " + id, e); + // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) + Optional gameVersion = profile.getRepository().getGameVersion(id); + String modPackVersion = null; + try { + ModpackConfiguration config = profile.getRepository().readModpackConfiguration(id); + modPackVersion = config != null ? config.getVersion() : null; + } catch (IOException e) { + LOG.warning("Failed to read modpack configuration from " + id, e); + } + return new Result(gameVersion.orElse(null), modPackVersion); + }, Schedulers.io()).whenCompleteAsync((result, exception) -> { + if (exception == null) { + if (result.gameVersion != null) { + title.set(result.gameVersion); + } + if (result.tag != null) { + tag.set(result.tag); + } + + StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion); + for (LibraryAnalyzer.LibraryMark mark : analyzer) { + String libraryId = mark.getLibraryId(); + String libraryVersion = mark.getLibraryVersion(); + if (libraryId.equals(MINECRAFT.getPatchId())) continue; + if (I18n.hasKey("install.installer." + libraryId)) { + libraries.append(", ").append(i18n("install.installer." + libraryId)); + if (libraryVersion != null) + libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); } + } - return new Result( - gameVersion.orElse(null), - modPackVersion - ); - }, Schedulers.io()) - .whenCompleteAsync((result, exception) -> { - if (exception == null) { - if (result.gameVersion != null) { - title.set(result.gameVersion); - } - if (result.tag != null) { - tag.set(result.tag); - } - - StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion); - for (LibraryAnalyzer.LibraryMark mark : analyzer) { - String libraryId = mark.getLibraryId(); - String libraryVersion = mark.getLibraryVersion(); - if (libraryId.equals(MINECRAFT.getPatchId())) continue; - if (I18n.hasKey("install.installer." + libraryId)) { - libraries.append(", ").append(i18n("install.installer." + libraryId)); - if (libraryVersion != null) - libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, "")); - } - } - - subtitle.set(libraries.toString()); - } else { - LOG.warning("Failed to read version info from " + id, exception); - } - }, Schedulers.javafx()); + subtitle.set(libraries.toString()); + } else { + LOG.warning("Failed to read version info from " + id, exception); + } + }, Schedulers.javafx()); title.set(id); image.set(profile.getRepository().getVersionIconImage(id)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 6ba210cf94..32a79c7c6e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -26,7 +26,6 @@ import javafx.scene.Cursor; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import javafx.scene.control.ToggleGroup; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; From f950c9b7c389231eefce26b364e2ca377a9684cb Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 15:38:25 +0800 Subject: [PATCH 18/24] update --- .../main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java | 1 + .../main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java index 03205fe203..c0cd486a5e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java @@ -60,6 +60,7 @@ public ToolbarListPageSkin2(P skinnable) { { this.listView = new JFXListView<>(); + this.listView.setPadding(Insets.EMPTY); ComponentList.setVgrow(listView, Priority.ALWAYS); Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); root.getContent().add(listView); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 9ae0f13ffc..5b09137dd6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -21,6 +21,7 @@ import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Priority; @@ -120,7 +121,6 @@ public ReadOnlyObjectProperty stateProperty() { private static class GameList extends ListPageBase { private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private boolean updatingSelection = false; public GameList() { Profiles.registerVersionsListener(profile -> FXUtils.runInFX(() -> loadVersions(profile))); From b477d1817e908502739803b551f0990a71c387be Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 15:40:42 +0800 Subject: [PATCH 19/24] update --- .../main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 5b09137dd6..1e7472a61d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -21,7 +21,6 @@ import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Priority; From 211eda769971ea48ac6b395ffaa85a5faf0c2cd7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 16:15:20 +0800 Subject: [PATCH 20/24] update --- .../java/org/jackhuang/hmcl/ui/versions/GameListCell.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 32a79c7c6e..83ea4be330 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -31,7 +31,6 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; @@ -89,13 +88,8 @@ public void fire() { center.setSpacing(8); center.setAlignment(Pos.CENTER_LEFT); - StackPane imageViewContainer = new StackPane(); - FXUtils.setLimitWidth(imageViewContainer, 32); - FXUtils.setLimitHeight(imageViewContainer, 32); - this.imageView = new ImageView(); FXUtils.limitSize(imageView, 32, 32); - imageViewContainer.getChildren().setAll(imageView); this.content = new TwoLineListItem(); BorderPane.setAlignment(content, Pos.CENTER); From 3a81301d3acb017abd2f37d2f8bdf016c6c793e3 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 20:41:34 +0800 Subject: [PATCH 21/24] update --- .../main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 1e7472a61d..1d94e24a36 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -122,7 +122,7 @@ private static class GameList extends ListPageBase { private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); public GameList() { - Profiles.registerVersionsListener(profile -> FXUtils.runInFX(() -> loadVersions(profile))); + Profiles.registerVersionsListener(this::loadVersions); setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); } From df344995f9d296375c57fbb0c4e3c16e2f2eb6ce Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 20:45:47 +0800 Subject: [PATCH 22/24] update --- .../hmcl/ui/versions/GameListCell.java | 65 ++++++------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java index 83ea4be330..28fa36979d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java @@ -25,7 +25,6 @@ import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; @@ -36,8 +35,6 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.StringUtils; -import java.util.function.Consumer; - import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -138,9 +135,7 @@ public void fire() { if (item == null) return; - preparePopupMenu(item); - - JFXPopup popup = getPopup(getListView()); + JFXPopup popup = getPopup(item); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); }); @@ -162,9 +157,7 @@ public void fire() { item.modifyGameSettings(); } } else if (e.getButton() == MouseButton.SECONDARY) { - preparePopupMenu(item); - - JFXPopup popup = getPopup(getListView()); + JFXPopup popup = getPopup(item); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); popup.show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); } @@ -198,42 +191,22 @@ public void updateItem(GameListItem item, boolean empty) { } } - // Popup Menu - - private static final String POPUP_ITEM_KEY = GameListCell.class.getName() + ".popup.item"; - - private void preparePopupMenu(GameListItem item) { - this.getListView().getProperties().put(POPUP_ITEM_KEY, item); - } - - private static Runnable getAction(ListView listView, Consumer action) { - return () -> { - if (listView.getProperties().get(POPUP_ITEM_KEY) instanceof GameListItem item) { - action.accept(item); - } - }; - } - - private static JFXPopup getPopup(ListView listView) { - return (JFXPopup) listView.getProperties().computeIfAbsent(GameListCell.class.getName() + ".popup", k -> { - PopupMenu menu = new PopupMenu(); - JFXPopup popup = new JFXPopup(menu); - - menu.getContent().setAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), getAction(listView, GameListItem::testGame), popup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), getAction(listView, GameListItem::generateLaunchScript), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), getAction(listView, GameListItem::modifyGameSettings), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), getAction(listView, GameListItem::rename), popup), - new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), getAction(listView, GameListItem::duplicate), popup), - new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), getAction(listView, GameListItem::remove), popup), - new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), getAction(listView, GameListItem::export), popup), - new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), getAction(listView, GameListItem::browse), popup)); - - popup.setOnHidden(event -> listView.getProperties().remove(POPUP_ITEM_KEY)); - return popup; - }); + private static JFXPopup getPopup(GameListItem item) { + PopupMenu menu = new PopupMenu(); + JFXPopup popup = new JFXPopup(menu); + + menu.getContent().setAll( + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), item::testGame, popup), + new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup), + new MenuSeparator(), + new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), item::modifyGameSettings, popup), + new MenuSeparator(), + new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), item::rename, popup), + new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), item::duplicate, popup), + new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), item::remove, popup), + new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), item::export, popup), + new MenuSeparator(), + new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), item::browse, popup)); + return popup; } } From cb7aa8c414adb4ced76343166a23c27008e69757 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 10 Jan 2026 20:47:40 +0800 Subject: [PATCH 23/24] update --- .../main/java/org/jackhuang/hmcl/ui/versions/GameItem.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 547d790b66..b9758888c1 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 @@ -30,12 +30,17 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; +import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class GameItem { + private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 10, TimeUnit.SECONDS); + protected final Profile profile; protected final String id; @@ -82,7 +87,7 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { LOG.warning("Failed to read modpack configuration from " + id, e); } return new Result(gameVersion.orElse(null), modPackVersion); - }, Schedulers.io()).whenCompleteAsync((result, exception) -> { + }, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> { if (exception == null) { if (result.gameVersion != null) { title.set(result.gameVersion); From db9cf8d5efc5cdad8955e67829f8a86756e04b4d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 11 Jan 2026 20:52:37 +0800 Subject: [PATCH 24/24] update --- .../org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java index bfbe678dc7..8210d86992 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java @@ -28,6 +28,7 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -49,7 +50,7 @@ public GameListPopupMenu() { this.setMaxHeight(365); this.getStyleClass().add("popup-menu-content"); - listView.setCellFactory(listView -> new Cell()); + listView.setCellFactory(Cell::new); listView.setFixedCellSize(60); listView.setPrefWidth(300); @@ -76,7 +77,7 @@ private static final class Cell extends ListCell { private final StringProperty tag = new SimpleStringProperty(); - public Cell() { + public Cell(ListView listView) { this.setPadding(Insets.EMPTY); HBox root = new HBox(); @@ -116,6 +117,7 @@ public Cell() { } }); this.graphic = ripplerContainer; + ripplerContainer.maxWidthProperty().bind(listView.widthProperty().subtract(5)); } @Override