feat: 实例列表搜索 (#5242)
Co-authored-by: From: zkitefly <z18344203426@qq.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user