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 a131974ed..cd8aede00 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 @@ -17,21 +17,34 @@ */ package org.jackhuang.hmcl.ui.versions; +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.*; 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.ListCell; import javafx.scene.control.ScrollPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +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.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; 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.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.ui.construct.ComponentList; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.profile.ProfileListItem; @@ -39,9 +52,14 @@ import org.jackhuang.hmcl.ui.profile.ProfilePage; import org.jackhuang.hmcl.util.FXThread; import org.jackhuang.hmcl.util.javafx.MappedObservableList; -import java.util.Collections; 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; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; @@ -60,8 +78,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage }); selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class); - GameList gameList = new GameList(); - { ScrollPane pane = new ScrollPane(); VBox.setVgrow(pane, Priority.ALWAYS); @@ -85,13 +101,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage AdvancedListBox bottomLeftCornerList = new AdvancedListBox() .addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame) .addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack) - .addNavigationDrawerItem(i18n("button.refresh"), SVG.REFRESH, gameList::refreshList) .addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings); - FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2); + FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2); setLeft(pane, bottomLeftCornerList); } - setCenter(gameList); + setCenter(new GameList()); } public ObjectProperty selectedProfileProperty() { @@ -122,7 +137,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage 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); + Profiles.registerVersionsListener(this::loadVersions); setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); @@ -133,44 +153,125 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage listenerHolder.clear(); setLoading(true); setFailedReason(null); - 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()) { + List versionItems = profile.getRepository().getDisplayVersions().map(instance -> new GameListItem(profile, instance.getId())).toList(); + + sourceList.setAll(versionItems); + + if (versionItems.isEmpty()) { setFailedReason(i18n("version.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.id).find(); + } catch (PatternSyntaxException e) { + return item -> false; + } + } else { + return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); + } + } + public void refreshList() { Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start(); } @Override - protected GameListSkin createDefaultSkin() { - return new GameListSkin(); + protected Skin createDefaultSkin() { + return new GameListSkin(this); } - private class GameListSkin extends ToolbarListPageSkin { + private static class GameListSkin extends SkinBase { + private final TransitionPane toolbarPane; + private final HBox searchBar; + private final HBox toolbarNormal; - public GameListSkin() { - super(GameList.this); + private final JFXTextField searchField; + + 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<>(); + + { + 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) -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { + changeToolbar(toolbarNormal); + searchField.clear(); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))); + + toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE); + + 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(x -> new GameListCell()); + listView.setItems(skinnable.getItems()); + + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); } - @Override - protected List initializeToolbar(GameList skinnable) { - return Collections.emptyList(); - } - - @Override - protected ListCell createListCell(JFXListView listView) { - return new GameListCell(); + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + runInFX(searchField::requestFocus); + } + } } } } - }