Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
234 changes: 183 additions & 51 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,217 @@

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);
}

if (selectingBtns != null && selectingBtns.length > 0) {
selectingToolbar.setAlignment(Pos.CENTER_LEFT);
selectingToolbar.setPickOnBounds(false);
selectingToolbar.getChildren().setAll(selectingBtns);
}

SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
toolbarPane.setContent(normalToolbar, ContainerAnimations.FADE);
FXUtils.setOverflowHidden(toolbarPane, 8);
rootList.getContent().add(toolbarPane);

ComponentList.setVgrow(spinnerPane, Priority.ALWAYS);
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);
}
};

{
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);
isSearching.addListener(toolbarSwitchListener);
isSelecting.addListener(toolbarSwitchListener);

spinnerPane.setContent(listView);
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 JFXButton createSelectAllButton() {
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);
return selectAll;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

createSelectAllButton 中,存在两个可以改进的地方:\n1. 逻辑漏洞:当列表为空时,!listView.getItems().isEmpty()false,导致整个表达式计算为 false,从而使 selectAll 按钮被启用setDisable(false))。实际上,当列表为空时,应该禁用“全选”按钮。建议将逻辑改为 listView.getItems().isEmpty() || ...。\n2. 初始状态未更新:在创建按钮并添加监听器后,没有立即触发一次状态更新。如果列表初始为空或已全选,按钮的禁用状态不会正确反映,直到下一次选择或列表内容发生变化。\n\n建议重构该方法,提取更新逻辑并进行初始调用。

    protected JFXButton createSelectAllButton() {\n        JFXButton selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));\n        Runnable updateDisableState = () -> {\n            selectAll.setDisable(listView.getItems().isEmpty()\n                    || listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());\n        };\n        ListChangeListener<Object> listener = change -> updateDisableState.run();\n        listView.getSelectionModel().getSelectedItems().addListener(listener);\n        listView.getItems().addListener(listener);\n        updateDisableState.run();\n        return selectAll;\n    }


root.getContent().add(spinnerPane);
protected JFXButton createCancelSelectionButton() {
return createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> listView.getSelectionModel().clearSelection());
}

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

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

public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
Expand All @@ -99,22 +249,4 @@ public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable
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