feat: 实例列表搜索 (#5242)

Co-authored-by: From: zkitefly <z18344203426@qq.com>
This commit is contained in:
辞庐
2026-02-17 23:27:35 +08:00
committed by GitHub
parent fb0919f3fb
commit 3187c28df0

View File

@@ -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<Profile> selectedProfileProperty() {
@@ -122,7 +137,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
private static class GameList extends ListPageBase<GameListItem> {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final ObservableList<GameListItem> sourceList = FXCollections.observableArrayList();
private final FilteredList<GameListItem> 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<GameListItem> children = FXCollections.observableList(profile.getRepository().getDisplayVersions()
.map(instance -> new GameListItem(profile, instance.getId()))
.toList());
setItems(children);
if (children.isEmpty()) {
List<GameListItem> 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<GameListItem> 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<GameListItem, GameList> {
private static class GameListSkin extends SkinBase<GameList> {
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<GameListItem> 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<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
@Override
protected ListCell<GameListItem> createListCell(JFXListView<GameListItem> 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);
}
}
}
}
}
}