Skip to content
240 changes: 180 additions & 60 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,67 +19,214 @@

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
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.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.StringUtils;

import java.util.List;
import java.util.function.Predicate;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends SkinBase<P> {

protected final StackPane mainContainer;
protected final ComponentList rootList;
protected final JFXListView<E> listView;
protected FilteredList<E> filteredList;

protected final TransitionPane toolbarPane;
protected final HBox normalToolbar;
protected final HBox selectingToolbar;
protected final HBox searchBar;
protected JFXTextField searchField;

protected final BooleanProperty isSearching = new SimpleBooleanProperty(false);
protected final BooleanProperty isSelecting = new SimpleBooleanProperty(false);

public ToolbarListPageSkin(P skinnable) {
public ToolbarListPageSkin(P skinnable, boolean hasSearch) {
super(skinnable);

ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");

StackPane container = new StackPane();
container.getChildren().add(root);
StackPane.setMargin(root, new Insets(10));

List<Node> toolbarButtons = initializeToolbar(skinnable);
if (!toolbarButtons.isEmpty()) {
HBox toolbar = new HBox();
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(toolbarButtons);
FXUtils.setOverflowHidden(toolbar, 8);
root.getContent().add(toolbar);
mainContainer = new StackPane();
mainContainer.setPadding(new Insets(10));
mainContainer.getStyleClass().addAll("notice-pane");

rootList = new ComponentList();
rootList.getStyleClass().add("no-padding");

listView = new JFXListView<>();
listView.setPadding(Insets.EMPTY);
listView.getStyleClass().add("no-horizontal-scrollbar");
Comment thread
CiiLu marked this conversation as resolved.
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);

toolbarPane = new TransitionPane();
normalToolbar = new HBox();
selectingToolbar = new HBox();
searchBar = new HBox();

if (hasSearch) {
filteredList = new FilteredList<>(skinnable.getItems());
listView.setItems(filteredList);

searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);

PauseTransition searchPause = new PauseTransition(Duration.millis(100));
searchPause.setOnFinished(e -> {
if (filteredList != null) {
filteredList.setPredicate(updateSearchPredicate(searchField.getText()));
}
});

searchField.textProperty().addListener((observable, oldValue, newValue) -> {
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});

JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
searchField.clear();
searchPause.stop();
filteredList.setPredicate(null);
isSearching.set(false);
});

FXUtils.onEscPressed(searchField, closeSearchBar::fire);

searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchBar.getChildren().setAll(searchField, closeSearchBar);
} else {
Bindings.bindContent(listView.getItems(), skinnable.itemsProperty());
}
}

protected void setupSkin(Node[] normalBtns, Node[] selectingBtns) {
if (normalBtns != null && normalBtns.length > 0) {
normalToolbar.setAlignment(Pos.CENTER_LEFT);
normalToolbar.setPickOnBounds(false);
normalToolbar.getChildren().setAll(normalBtns);
}

SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
if (selectingBtns != null && selectingBtns.length > 0) {
selectingToolbar.setAlignment(Pos.CENTER_LEFT);
selectingToolbar.setPickOnBounds(false);
selectingToolbar.getChildren().setAll(selectingBtns);

ComponentList.setVgrow(spinnerPane, Priority.ALWAYS);
JFXButton cancel = createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> listView.getSelectionModel().clearSelection());

{
this.listView = new JFXListView<>();
this.listView.setPadding(Insets.EMPTY);
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
this.listView.getStyleClass().add("no-horizontal-scrollbar");
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
JFXButton selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
ListChangeListener<Object> listener = change -> {
selectAll.setDisable(!listView.getItems().isEmpty()
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
};
listView.getSelectionModel().getSelectedItems().addListener(listener);
listView.getItems().addListener(listener);

spinnerPane.setContent(listView);
selectingToolbar.getChildren().addAll(selectAll, cancel);
}

root.getContent().add(spinnerPane);
toolbarPane.setContent(normalToolbar, ContainerAnimations.FADE);
FXUtils.setOverflowHidden(toolbarPane, 8);
rootList.getContent().add(toolbarPane);

getChildren().setAll(container);
rootList.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
if (listView.getSelectionModel().getSelectedItem() != null) {
listView.getSelectionModel().clearSelection();
e.consume();
}
}
});

SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(getSkinnable().loadingProperty());
center.failedReasonProperty().bind(getSkinnable().failedReasonProperty());
center.onFailedActionProperty().bind(getSkinnable().onFailedActionProperty());
center.setContent(listView);
rootList.getContent().add(center);

StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label();
placeholderLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (isSearching.get()) return i18n("search.no_results_found");
return getEmptyPlaceholderText();
}, isSearching));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);

listView.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> {
isSelecting.set(newV != null);
});

InvalidationListener toolbarSwitchListener = obs -> {
if (isSelecting.get() && selectingBtns != null && selectingBtns.length > 0) {
changeToolbar(selectingToolbar);
} else if (isSearching.get() && hasSearch()) {
changeToolbar(searchBar);
} else {
changeToolbar(normalToolbar);
}
};

isSearching.addListener(toolbarSwitchListener);
isSelecting.addListener(toolbarSwitchListener);

toolbarSwitchListener.invalidated(null);

mainContainer.getChildren().add(rootList);
getChildren().setAll(mainContainer);
}

protected void changeToolbar(HBox newToolbar) {
Node oldToolbar = toolbarPane.getCurrentNode();
if (newToolbar != oldToolbar) {
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
if (newToolbar == searchBar && searchField != null) {
Platform.runLater(searchField::requestFocus);
}
}
}

protected boolean hasSearch() {
return searchField != null;
}

protected void startSearch() {
isSearching.set(true);
}

protected Predicate<E> updateSearchPredicate(String query) {
return item -> true;
}

protected String getEmptyPlaceholderText() {
return i18n("mods.empty");
}

public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
Expand All @@ -90,31 +237,4 @@ public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onCl
ret.setOnAction(e -> onClick.run());
return ret;
}

public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.setGraphic(svg.createIcon(20));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnAction(e -> onClick.run());
return ret;
}

protected abstract List<Node> initializeToolbar(P skinnable);

protected ListCell<E> createListCell(JFXListView<E> listView) {
return new ListCell<>() {
@Override
protected void updateItem(E item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item instanceof Node node) {
setGraphic(node);
setText(null);
} else {
setGraphic(null);
setText(null);
}
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,20 @@
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.addon.datapack.DataPack;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
Expand Down Expand Up @@ -127,29 +122,4 @@ void disableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> sel
void openDataPackFolder() {
FXUtils.openFolder(dataPack.getPath());
}

@NotNull Predicate<DataPackListPageSkin.DataPackInfoObject> updateSearchPredicate(String queryString) {
if (queryString.isBlank()) {
return dataPack -> true;
}

final Predicate<String> stringPredicate;
if (queryString.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(StringUtils.substringAfter(queryString, "regex:"));
stringPredicate = s -> s != null && pattern.matcher(s).find();
} catch (Exception e) {
return dataPack -> false;
}
} else {
String lowerCaseFilter = queryString.toLowerCase(Locale.ROOT);
stringPredicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerCaseFilter);
}

return dataPack -> {
String id = dataPack.getPackInfo().getId();
String description = dataPack.getPackInfo().getDescription().toString();
return stringPredicate.test(id) || stringPredicate.test(description);
};
}
}
Loading