From 3a6e80da0e2e2616f27e58e25f6ffc1dc822f148 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Sun, 8 Nov 2026 14:57:06 +0800 Subject: [PATCH] refactor: remove multi-instance list pages and unused wizard pages Co-Authored-By: Claude Sonnet 5.5 --- .../org/jackhuang/hmcl/ui/Controllers.java | 24 -- .../hmcl/ui/download/InstallersPage.java | 139 ---------- .../jackhuang/hmcl/ui/instances/GameItem.java | 132 ---------- .../hmcl/ui/instances/GameListCell.java | 191 -------------- .../hmcl/ui/instances/GameListItem.java | 77 ------ .../hmcl/ui/instances/GameListPage.java | 248 ------------------ .../hmcl/ui/instances/GameListPopupMenu.java | 187 ------------- .../org/jackhuang/hmcl/ui/main/MainPage.java | 1 - .../org/jackhuang/hmcl/ui/main/RootPage.java | 19 +- 9 files changed, 1 insertion(+), 1017 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index b10a8db0e9..dd0ac070d3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -39,7 +39,6 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.decorator.Decorator; import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; -import org.jackhuang.hmcl.ui.instances.GameListPage; import org.jackhuang.hmcl.ui.instances.GameInstancePage; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.upgrade.IntegrityChecker; @@ -128,28 +127,6 @@ public final class Controllers { return gameInstancePage; } - /// The cached game-list page. - private static @Nullable GameListPage gameListPage; - - /// Creates and attaches the game-list page for a layout pulse unless already cached. - @FXThread - public static void prepareGameListPage() { - if (gameListPage == null) { - LOG.info("Preparing game list page"); - gameListPage = getDecorator().prewarmPage(new GameListPage()); - } - } - - /// Returns the cached game-list page, creating it if necessary. - @FXThread - public static GameListPage getGameListPage() { - if (gameListPage == null) { - gameListPage = new GameListPage(); - } - return gameListPage; - } - - /// The cached root page. private static @Nullable RootPage rootPage; @@ -694,7 +671,6 @@ public final class Controllers { onApplicationStop(); rootPage = null; gameInstancePage = null; - gameListPage = null; accountListPage = null; settingsPage = null; decorator = null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java deleted file mode 100644 index 213aaad28d..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.download; - -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.InstallerItem; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.RequiredValidator; -import org.jackhuang.hmcl.ui.construct.Validator; -import org.jackhuang.hmcl.ui.wizard.WizardController; -import org.jackhuang.hmcl.util.SettingsMap; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; - -import static javafx.beans.binding.Bindings.createBooleanBinding; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class InstallersPage extends AbstractInstallersPage { - - private boolean isNameModifiedByUser = false; - - public InstallersPage(WizardController controller, HMCLGameRepository repository, GameVersionNumber gameVersion, DownloadProvider downloadProvider) { - super(controller, gameVersion, downloadProvider); - - txtName.getValidators().addAll( - new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)), - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId)); - installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty())); - - txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true); - } - - @Override - public String getTitle() { - return ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion().toString(); - } - - private String getVersion(String id) { - return I18n.getDisplayVersion((ComponentRemoteVersion) controller.getSettings().get(id)); - } - - protected void reload() { - for (InstallerItem component : group.getComponents()) { - String libraryId = component.getComponentType().getPatchId(); - if (controller.getSettings().containsKey(libraryId)) { - component.versionProperty().set(new InstallerItem.InstalledState(getVersion(libraryId), false, false)); - } else { - component.versionProperty().set(null); - } - } - if (!isNameModifiedByUser) { - setTxtNameWithLoaders(); - } - } - - @Override - public void cleanup(SettingsMap settings) { - } - - private static boolean checkName(String name) { - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - if (!(c >= '0' && c <= '9') - && !(c >= 'a' && c <= 'z') - && !(c >= 'A' && c <= 'Z') - && c != '-' && c != '_' && c != '.' - ) - return false; - } - - return true; - } - - protected void onInstall() { - GameInstanceID instanceId = new GameInstanceID(txtName.getText()); - - if (!checkName(instanceId.id())) { - Controllers.dialog(new MessageDialogPane.Builder( - i18n("install.name.invalid"), - i18n("message.warning"), - MessageDialogPane.MessageType.QUESTION) - .yesOrNo(() -> { - controller.getSettings().put(INSTANCE_ID, instanceId); - controller.onFinish(); - }, () -> { - // The user selects Cancel and does nothing. - }) - .build()); - } else { - controller.getSettings().put(INSTANCE_ID, instanceId); - controller.onFinish(); - } - } - - private void setTxtNameWithLoaders() { - StringBuilder nameBuilder = new StringBuilder(getTitle()); - - for (InstallerItem component : group.getComponents()) { - if (component.getComponentType() == GameComponentType.GAME - || !controller.getSettings().containsKey(component.getComponentType().getPatchId())) - continue; - - String loaderName = switch (component.getComponentType()) { - case FORGE -> "Forge"; - case NEO_FORGE -> "NeoForge"; - case CLEANROOM -> "Cleanroom"; - case LEGACY_FABRIC -> "LegacyFabric"; - case FABRIC -> "Fabric"; - case LITELOADER -> "LiteLoader"; - case QUILT -> "Quilt"; - case OPTIFINE -> "OptiFine"; - default -> null; - }; - - if (loaderName != null) - nameBuilder.append('-').append(loaderName); - } - - txtName.setText(nameBuilder.toString()); - isNameModifiedByUser = false; - } - - @Override - protected boolean showExtendPane() { - return true; - } - - @Override - protected void resetDefaultName() { - isNameModifiedByUser = false; - setTxtNameWithLoaders(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java deleted file mode 100644 index 396a1d0e2e..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.property.*; -import javafx.scene.image.Image; -import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.modpack.ModpackConfiguration; -import org.jackhuang.hmcl.setting.GameDirectory; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -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 HMCLGameInstance gameInstance; - - private boolean initialized = false; - private StringProperty title; - private StringProperty tag; - private StringProperty subtitle; - private ObjectProperty image; - - public GameItem(HMCLGameInstance gameInstance) { - this.gameInstance = gameInstance; - } - - public GameDirectory getGameDirectory() { - return gameInstance.getRepository().getGameDirectory(); - } - - public HMCLGameRepository getRepository() { - return gameInstance.getRepository(); - } - - public GameInstanceID getInstanceId() { - return gameInstance.getId(); - } - - public HMCLGameInstance getGameInstance() { - return gameInstance; - } - - public String getId() { - return gameInstance.getId().toString(); - } - - 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(() -> { - // Version detection performs blocking JAR reads and may hash the entire file. - GameVersionNumber version = gameInstance.getVersion(); - @Nullable String gameVersion = version == GameVersionNumber.unknown() ? null : version.toString(); - @Nullable String modPackVersion = null; - try { - @Nullable ModpackConfiguration config = gameInstance.readModpackConfiguration(); - modPackVersion = config != null ? config.getVersion() : null; - } catch (IOException e) { - LOG.warning("Failed to read modpack configuration from " + getId(), e); - } - return new Result(gameVersion, modPackVersion); - }, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> { - if (exception == null) { - if (result.tag != null) { - tag.set(result.tag); - } - - StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); - GameComponentAnalyzer analyzer = gameInstance.getAnalyzer(); - for (GameComponentAnalyzer.Mark mark : analyzer) { - if (mark.componentType() == GameComponentType.GAME) continue; - - if (I18n.hasKey("install.installer." + mark.componentType().getPatchId())) { - libraries.append(", ").append(i18n("install.installer." + mark.componentType().getPatchId())); - if (mark.version() != null) - libraries.append(": ").append(mark.version().replaceAll("(?i)" + mark.componentType().getPatchId(), "")); - } - } - - subtitle.set(libraries.toString()); - } else { - LOG.warning("Failed to read version info from " + getId(), exception); - } - }, Schedulers.javafx()); - - title.set(getId()); - image.set(gameInstance.getIconImage()); - } - - 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/instances/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java deleted file mode 100644 index bd7c713d65..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -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.input.MouseButton; -import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.StringUtils; -import org.jetbrains.annotations.Nullable; - -import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public final class GameListCell extends ListCell { - - private final RipplerContainer graphic; - - private final ImageContainer imageView; - private final TwoLineListItem content; - - private final JFXRadioButton chkSelected; - private final JFXButton btnUpgrade; - /// Opens the settings for this cell's game instance. - private final JFXButton btnEditInstance; - private final JFXButton btnManage; - - private final HBox right; - - private final StringProperty tag = new SimpleStringProperty(); - - /// Creates a game instance cell constrained to the supplied list's viewport width. - public GameListCell(ListView listView) { - FXUtils.limitCellWidth(listView, this); - - TwoLineListItem root = this.content = new TwoLineListItem(); - 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() { - /// Selects this cell's game instance when the radio button is enabled and unselected. - @Override - public void fire() { - if (!isDisable() && !isSelected()) { - fireEvent(new ActionEvent()); - @Nullable GameListItem item = GameListCell.this.getItem(); - if (item != null) { - item.getRepository().setSelectedInstance(item.getGameInstance()); - } - } - } - }; - } - - { - this.imageView = new ImageContainer(32); - imageView.setMouseTransparent(true); - HBox leading = new HBox(8, chkSelected, imageView); - leading.setAlignment(Pos.CENTER_LEFT); - leading.setPickOnBounds(false); - TwoLineListItem.setMargin(leading, new Insets(0, 8, 0, 0)); - content.setLeading(leading); - - FXUtils.onChangeAndOperate(tag, tag -> { - if (StringUtils.isNotBlank(tag)) - content.getTags().setAll(new Tag(tag)); - else - content.getTags().clear(); - }); - } - - { - this.right = new HBox(); - content.setTrailing(right); - right.setAlignment(Pos.CENTER_RIGHT); - right.setPickOnBounds(false); - - this.btnUpgrade = FXUtils.newToggleButton4(SVG.UPDATE); - btnUpgrade.setOnAction(e -> { - @Nullable GameListItem item = this.getItem(); - if (item != null) - item.update(); - }); - FXUtils.installFastTooltip(btnUpgrade, i18n("instance.update")); - right.getChildren().add(btnUpgrade); - - this.btnEditInstance = FXUtils.newToggleButton4(SVG.SETTINGS); - btnEditInstance.setOnAction(e -> { - @Nullable GameListItem item = this.getItem(); - if (item != null) - item.modifyGameSettings(); - }); - FXUtils.installFastTooltip(btnEditInstance, i18n("instance.manage.manage")); - right.getChildren().add(btnEditInstance); - - this.btnManage = FXUtils.newToggleButton4(SVG.MORE_VERT); - btnManage.setOnAction(e -> { - @Nullable GameListItem item = this.getItem(); - if (item == null) - return; - - 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()); - }); - FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")); - right.getChildren().add(btnManage); - } - - root.setCursor(Cursor.HAND); - container.setOnMouseClicked(e -> { - @Nullable GameListItem item = getItem(); - if (item == null) - return; - - if (e.getButton() == MouseButton.PRIMARY) { - if (e.getClickCount() == 1) { - item.getRepository().setSelectedInstance(item.getGameInstance()); - } - } else if (e.getButton() == MouseButton.SECONDARY) { - 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()); - } - }); - } - - @Override - public void updateItem(GameListItem item, boolean empty) { - GameListItem oldItem = getItem(); - boolean oldEmpty = isEmpty(); - - super.updateItem(item, empty); - - if (oldItem == item && oldEmpty == empty) return; - - this.graphic.resetFeedback(); - - this.imageView.imageProperty().unbind(); - 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.chkSelected.selectedProperty().bind(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(btnEditInstance, btnManage); - } - } - - private static JFXPopup getPopup(GameListItem item) { - PopupMenu menu = new PopupMenu(); - JFXPopup popup = new JFXPopup(menu); - - menu.getContent().setAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), item::testGame, popup), - new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), item::generateLaunchScript, popup), - new MenuSeparator(), - new IconedMenuItem(SVG.EDIT, i18n("instance.manage.rename"), item::rename, popup), - new IconedMenuItem(SVG.FOLDER_COPY, i18n("instance.manage.duplicate"), item::duplicate, popup), - new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), item::remove, popup), - new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), item::browse, popup)); - return popup; - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java deleted file mode 100644 index d155e63111..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import org.jackhuang.hmcl.game.GameInstanceID; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.setting.GameDirectoryManager; -import org.jetbrains.annotations.Nullable; - -public class GameListItem extends GameItem { - private final boolean isModpack; - private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected"); - - public GameListItem(HMCLGameInstance gameInstance) { - super(gameInstance); - HMCLGameRepository repository = gameInstance.getRepository(); - GameInstanceID instanceId = gameInstance.getId(); - this.isModpack = gameInstance.isModpack(); - selected.bind(Bindings.createBooleanBinding( - () -> { - if (repository.getGameDirectory() != GameDirectoryManager.getSelectedGameDirectory()) return false; - @Nullable HMCLGameInstance selectedInstance = repository.getSelectedInstance(); - return selectedInstance != null && selectedInstance.getId().equals(instanceId); - }, - GameDirectoryManager.selectedGameDirectoryProperty(), - GameDirectoryManager.selectedInstanceProperty())); - } - - public ReadOnlyBooleanProperty selectedProperty() { - return selected; - } - - public void rename() { - Instances.renameInstance(gameInstance); - } - - public void duplicate() { - Instances.duplicateInstance(gameInstance); - } - - public void remove() { - Instances.deleteInstance(gameInstance); - } - - public void browse() { - Instances.openFolder(gameInstance); - } - - public void testGame() { - Instances.testGame(gameInstance); - } - - public void launch() { - Instances.launch(gameInstance); - } - - public void modifyGameSettings() { - Instances.modifyGameSettings(gameInstance); - } - - public void generateLaunchScript() { - Instances.generateLaunchScript(gameInstance); - } - - public boolean canUpdate() { - return isModpack; - } - - public void update() { - Instances.updateInstance(gameInstance); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java deleted file mode 100644 index 9083161e94..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXTextField; -import javafx.animation.PauseTransition; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.*; -import javafx.util.Duration; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.ModpackHelper; -import org.jackhuang.hmcl.setting.GameDirectoryManager; -import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.util.FXThread; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.FileUtils; - -import java.nio.file.Path; -import java.util.List; -import java.util.Locale; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import static org.jackhuang.hmcl.ui.FXUtils.*; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("instance.manage"))); - - /// Creates the fixed-directory instance list. - public GameListPage() { - getSidebar() - .addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings); - - setContent(new GameList()); - - FXUtils.applyDragListener(this, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> { - Path file = files.get(0); - - if (ModpackHelper.isFileModpackByExtension(file)) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file), i18n("install.modpack")); - } else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) { - Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file); - } - }); - } - - public void modifyGlobalGameSettings() { - Instances.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository()); - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state.getReadOnlyProperty(); - } - - private static class GameList extends ListPageBase { - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - - private final ObservableList sourceList = FXCollections.observableArrayList(); - private final FilteredList filteredList = new FilteredList<>(sourceList); - - public GameList() { - setItems(filteredList); - - GameDirectoryManager.registerVersionsListener(this::loadVersions); - - setOnFailedAction(e -> Instances.installServerModpack()); - } - - @FXThread - private void loadVersions(HMCLGameRepository repository) { - listenerHolder.clear(); - setLoading(true); - setFailedReason(null); - - List instanceItems = repository.getDisplayInstances() - .map(GameListItem::new) - .toList(); - - sourceList.setAll(instanceItems); - - if (instanceItems.isEmpty()) { - setFailedReason(i18n("instance.empty.hint")); - } - - setLoading(false); - } - - private Predicate createPredicate(String searchText) { - if (searchText == null || searchText.isEmpty()) { - return item -> true; - } - - if (searchText.startsWith("regex:")) { - String regex = searchText.substring("regex:".length()); - try { - Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); - return item -> pattern.matcher(item.getId()).find(); - } catch (PatternSyntaxException e) { - return item -> false; - } - } else { - return item -> item.getId().toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); - } - } - - public void refreshList() { - GameDirectoryManager.getSelectedRepository().refreshAsync().start(); - } - - @Override - protected Skin createDefaultSkin() { - return new GameListSkin(this); - } - - private static class GameListSkin extends SkinBase { - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox toolbarNormal; - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - private final JFXTextField searchField; - - /// Creates the instance list with search controls and viewport-constrained rows. - public GameListSkin(GameList skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - JFXListView listView = new JFXListView<>(); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - { - toolbarPane = new TransitionPane(); - - searchBar = new HBox(); - toolbarNormal = new HBox(); - - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(e -> skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText()))); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - pause.setRate(1); - pause.playFromStart(); - } - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { - changeToolbar(toolbarNormal); - searchField.clear(); - - pause.stop(); - skinnable.filteredList.setPredicate(null); - isSearching.set(false); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("install.modpack"), SVG.PACKAGE2, Instances::installServerModpack), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))); - - toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE); - - FXUtils.setOverflowHidden(toolbarPane, 8); - - root.getContent().add(toolbarPane); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - center.failedReasonProperty().bind(skinnable.failedReasonProperty()); - - listView.setCellFactory(GameListCell::new); - listView.setItems(skinnable.getItems()); - - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(); - placeholderLabel.textProperty().bind( - Bindings.when(isSearching).then(i18n("search.no_results_found")).otherwise("") - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); - } - - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - runInFX(searchField::requestFocus); - - isSearching.set(true); - } - } - } - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java deleted file mode 100644 index 05c737281b..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -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.Insets; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.stage.WindowEvent; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.ImageContainer; -import org.jackhuang.hmcl.ui.construct.RipplerContainer; -import org.jackhuang.hmcl.ui.construct.Tag; -import org.jackhuang.hmcl.ui.construct.TwoLineListItem; -import org.jackhuang.hmcl.util.StringUtils; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -/// Displays game instances in a popup selection list. -/// -/// @author Glavo -public final class GameListPopupMenu extends StackPane { - - private static final String KEY = GameListPopupMenu.class.getName() + ".popup"; - - public static boolean hideShowing(Node owner) { - JFXPopup popup = (JFXPopup) owner.getProperties().get(KEY); - if (popup != null && popup.isShowing()) { - popup.hide(); - return true; - } else { - return false; - } - } - - /// Shows an instance selection popup relative to its owner. - /// - /// @param owner the node used to position the popup - /// @param vAlign the popup's vertical alignment relative to `owner` - /// @param hAlign the popup's horizontal alignment relative to `owner` - /// @param initOffsetX the horizontal offset from the aligned position - /// @param initOffsetY the vertical offset from the aligned position - /// @param instances the instances to copy into the popup, in display order - public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign, - double initOffsetX, double initOffsetY, - List instances) { - if (!hideShowing(owner)) { - showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances); - } - } - - /// Shows and returns an instance selection popup relative to its owner. - /// - /// @param owner the node used to position the popup - /// @param vAlign the popup's vertical alignment relative to `owner` - /// @param hAlign the popup's horizontal alignment relative to `owner` - /// @param initOffsetX the horizontal offset from the aligned position - /// @param initOffsetY the vertical offset from the aligned position - /// @param instances the instances to copy into the popup, in display order - /// @return the shown popup - public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign, - double initOffsetX, double initOffsetY, - List instances) { - GameListPopupMenu menu = new GameListPopupMenu(); - menu.getItems().setAll(instances.stream() - .map(GameItem::new) - .toList()); - JFXPopup popup = new JFXPopup(menu); - owner.getProperties().put(KEY, popup); - popup.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> owner.getProperties().remove(KEY, popup)); - popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY, true); - - return popup; - } - - private final JFXListView listView = new JFXListView<>(); - private final BooleanBinding isEmpty = Bindings.isEmpty(listView.getItems()); - - public GameListPopupMenu() { - this.setMaxHeight(365); - this.getStyleClass().add("popup-menu-content"); - - listView.setCellFactory(Cell::new); - - listView.setFixedCellSize(50); - listView.setPrefWidth(300); - - listView.prefHeightProperty().bind(Bindings.size(getItems()).multiply(50).add(2)); - - Label placeholder = new Label(i18n("instance.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 RipplerContainer ripplerContainer; - - private final ImageContainer imageView; - private final TwoLineListItem content; - - private final StringProperty tag = new SimpleStringProperty(); - - /// Creates a popup row with a leading icon and the shared advanced-item text styling. - public Cell(ListView listView) { - this.setPadding(Insets.EMPTY); - - this.imageView = new ImageContainer(32); - this.imageView.setMouseTransparent(true); - - this.content = new TwoLineListItem(); - content.getStyleClass().add("container"); - content.setLeading(imageView); - FXUtils.onChangeAndOperate(tag, tag -> { - if (StringUtils.isNotBlank(tag)) - content.getTags().setAll(new Tag(tag)); - else - content.getTags().clear(); - }); - - this.ripplerContainer = new RipplerContainer(content); - - StackPane rootPane = new StackPane(); - rootPane.getStyleClass().add("advanced-list-item"); - rootPane.getChildren().setAll(ripplerContainer); - rootPane.maxWidthProperty().bind(listView.widthProperty().subtract(5)); - - FXUtils.onClicked(rootPane, () -> { - @Nullable GameItem item = getItem(); - if (item != null) { - item.getRepository().setSelectedInstance(item.getGameInstance()); - if (getScene().getWindow() instanceof JFXPopup popup) - popup.hide(); - } - }); - - this.graphic = rootPane; - } - - @Override - protected void updateItem(GameItem item, boolean empty) { - GameItem oldItem = getItem(); - boolean oldEmpty = isEmpty(); - - super.updateItem(item, empty); - - if (oldItem == item && oldEmpty == empty) return; - - this.ripplerContainer.resetFeedback(); - 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()); - } - } - } -} 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 01a98f55a5..f44c00769c 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 @@ -35,7 +35,6 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.construct.IconedMenuItem; import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.Lang; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 3b806bfaca..7ee23d9369 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -21,7 +21,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem; -import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.io.FileUtils; @@ -53,17 +52,11 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { gameInstanceItem.setOnAction(e -> { @Nullable HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); if (instance == null) { - Controllers.navigate(Controllers.getGameListPage()); + Instances.installServerModpack(); } else { Instances.modifyGameSettings(instance); } }); - FXUtils.onScroll(gameInstanceItem, getMainPage().getInstances(), list -> { - @Nullable HMCLGameInstance currentGame = getMainPage().getCurrentGame(); - @Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null; - return Lang.indexWhere(list, instance -> instance.getId().equals(currentId)); - }, instance -> instance.getRepository().setSelectedInstance(instance)); - if (AnimationUtils.isAnimationEnabled()) { gameInstanceItem.setPrepareAction(Controllers::prepareGameInstancePage); } @@ -116,14 +109,4 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { } return mainPage; } - - /// Opens the instance selection menu beside the sidebar entry. - private void showGameListPopupMenu(Region gameListItem) { - GameListPopupMenu.show(gameListItem, - JFXPopup.PopupVPosition.TOP, - JFXPopup.PopupHPosition.LEFT, - gameListItem.getWidth(), - 0, - getMainPage().getInstances()); - } }