Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
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 @@ -37,8 +37,8 @@
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
Expand All @@ -50,16 +50,17 @@
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
Expand Down Expand Up @@ -193,11 +194,10 @@ private void loadJava(Collection<JavaRuntime> javaRuntimes) {
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaRuntime, JavaManagementPage> {

JavaPageSkin(JavaManagementPage skinnable) {
super(skinnable);
}
super(skinnable, false);

listView.setCellFactory(x -> new JavaItemCell(listView));

@Override
protected List<Node> initializeToolbar(JavaManagementPage skinnable) {
ArrayList<Node> res = new ArrayList<>(4);

res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh));
Expand All @@ -216,12 +216,7 @@ protected List<Node> initializeToolbar(JavaManagementPage skinnable) {
}
res.add(disableJava);

return res;
}

@Override
protected ListCell<JavaRuntime> createListCell(JFXListView<JavaRuntime> listView) {
return new JavaItemCell(listView);
setupSkin(res.toArray(new Node[0]), null);
}
}

Expand Down
16 changes: 6 additions & 10 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.java.JavaManager;
Expand All @@ -43,8 +45,6 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
Expand Down Expand Up @@ -188,12 +188,8 @@ private static final class DisabledJavaItemSkin extends SkinBase<DisabledJavaIte

private static final class JavaRestorePageSkin extends ToolbarListPageSkin<DisabledJavaItem, JavaRestorePage> {
JavaRestorePageSkin(JavaRestorePage skinnable) {
super(skinnable);
}

@Override
protected List<Node> initializeToolbar(JavaRestorePage skinnable) {
return Collections.emptyList();
super(skinnable, false);
setupSkin(null, null);
}
}
}
Loading
Loading