diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java new file mode 100644 index 0000000000..a55a2a32f5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java @@ -0,0 +1,128 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; + +public class CommonListPage extends ListPageBase { + private CellMenuRequestSupportType cellMenuRequestSupportType = CellMenuRequestSupportType.SINGLE; + private SelectionType selectionType = SelectionType.SINGLE; + + public CommonListPage() { + super(); + } + + private ObjectProperty>> onSingleCellMenuRequest; + + public ObjectProperty>> onSingleCellMenuRequestProperty() { + if (onSingleCellMenuRequest == null) { + onSingleCellMenuRequest = new SimpleObjectProperty<>(this, "onSingleCellMenuRequest") { + @Override + protected void invalidated() { + setEventHandler(CellMenuRequestEvent.SINGLE_CELL, get()); + } + }; + } + return onSingleCellMenuRequest; + } + + public void setOnSingleCellMenuRequest(EventHandler> onSingleCellMenuRequest) { + onSingleCellMenuRequestProperty().set(onSingleCellMenuRequest); + } + + private ObjectProperty>> onMutiCellMenuRequest; + + public ObjectProperty>> onMutiCellMenuRequestProperty() { + if (onMutiCellMenuRequest == null) { + onMutiCellMenuRequest = new SimpleObjectProperty<>(this, "onMutiCellMenuRequest") { + @Override + protected void invalidated() { + setEventHandler(CellMenuRequestEvent.MULTIPLE_CELL, get()); + } + }; + } + return onMutiCellMenuRequest; + } + + public void setOnMutiCellMenuRequest(EventHandler> onMutiCellMenuRequest) { + onMutiCellMenuRequestProperty().set(onMutiCellMenuRequest); + } + + public void setCellMenuRequestSupportType(CellMenuRequestSupportType cellMenuRequestSupportType) { + this.cellMenuRequestSupportType = cellMenuRequestSupportType; + } + + public CellMenuRequestSupportType getCellMenuRequestSupportType() { + return cellMenuRequestSupportType; + } + + public SelectionType getSelectionType() { + return selectionType; + } + + public void setSelectionType(SelectionType selectionType) { + this.selectionType = selectionType; + } + + public static final class CellMenuRequestEvent extends Event { + + public static final EventType> ANY = + new EventType<>(Event.ANY, "CELL_MENU_REQUEST"); + + public static final EventType> SINGLE_CELL = + new EventType<>(ANY, "SINGLE_CELL"); + + public static final EventType> MULTIPLE_CELL = + new EventType<>(ANY, "MULTIPLE_CELL"); + + private final ListCell listCell; + private final ListView listView; + + public CellMenuRequestEvent(EventType> eventType, ListCell listCell, ListView listView) { + super(eventType); + this.listCell = listCell; + this.listView = listView; + } + + public ListCell getListCell() { + return listCell; + } + + public ListView getListView() { + return listView; + } + } + + public enum CellMenuRequestSupportType { + SINGLE, + MULTIPLE, + BOTH + } + + public enum SelectionType { + SINGLE, + MULTIPLE, + NONE + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java new file mode 100644 index 0000000000..3906693098 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -0,0 +1,300 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXListView; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ListCell; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.SkinBase; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.CommonMDListCell; +import org.jackhuang.hmcl.ui.construct.ComponentList; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +public abstract class CommonListPageSkin extends SkinBase> { + + private final JFXListView listView = new JFXListView<>(); + private final TransitionPane toolbarPane = new TransitionPane(); + + private final AtomicInteger lastNotShiftClickIndex = new AtomicInteger(-1); + private final AtomicBoolean requestMenu = new AtomicBoolean(false); + + public CommonListPageSkin(CommonListPage skinnable) { + super(skinnable); + initPane(); + } + + public CommonListPageSkin(CommonListPage skinnable, CommonListPage.SelectionType selectionType) { + super(skinnable); + skinnable.setSelectionType(selectionType); + initPane(); + } + + private void initPane() { + + StackPane pagePane = new StackPane(); + { + pagePane.setPadding(new Insets(10)); + getChildren().setAll(pagePane); + } + + ComponentList rootPane = new ComponentList(); + { + rootPane.getStyleClass().add("no-padding"); + pagePane.getChildren().setAll(rootPane); + + rootPane.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + { + toolbarPane.disableProperty().bind(getSkinnable().loadingProperty()); + SpinnerPane center = new SpinnerPane(); + { + ComponentList.setVgrow(center, Priority.ALWAYS); + center.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(getSkinnable().loadingProperty()); + center.failedReasonProperty().bind(getSkinnable().failedReasonProperty()); + center.onFailedActionProperty().bind(getSkinnable().onFailedActionProperty()); + + rootPane.getContent().addAll(toolbarPane, center); + } + { + // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here + FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + listView.setCellFactory(listView -> createListCell(getListView())); + if (getSkinnable().getSelectionType() == CommonListPage.SelectionType.MULTIPLE) { + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + } else { + listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + } + this.listView.itemsProperty().bind(getSkinnable().itemsProperty()); + center.setContent(listView); + } + } + + FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), (newValue) -> lastNotShiftClickIndex.set(-1)); + FXUtils.onChangeAndOperate(getSkinnable().itemsProperty(), (newValue) -> lastNotShiftClickIndex.set(-1)); + } + + public void setToolbar(Node toolbar) { + Node oldToolbar = getToolBar().getCurrentNode(); + if (toolbar != oldToolbar) { + toolbarPane.setContent(toolbar, ContainerAnimations.FADE); + } + } + + public TransitionPane getToolBar() { + return toolbarPane; + } + + public JFXListView getListView() { + return listView; + } + + public ObservableList getSelectedItems() { + return listView.getSelectionModel().getSelectedItems(); + } + + public T getSelectedItem() { + return listView.getSelectionModel().getSelectedItems().get(0); + } + + public ReadOnlyObjectProperty selectedItemProperty() { + return listView.getSelectionModel().selectedItemProperty(); + } + + // Override this method to customize the cell rendering. + // Default: Renders the item as a Node if possible. + public CommonMDListCell listCell(JFXListView listView) { + return new CommonMDListCell<>(listView) { + + @Override + protected void updateControl(T item, boolean empty) { + if (!empty && item instanceof Node node) { + getContainer().getChildren().setAll(node); + } + } + }; + } + + private ListCell createListCell(JFXListView listView) { + CommonMDListCell commonMDListCell = listCell(listView); + switch (getSkinnable().getSelectionType()) { + case SINGLE -> { + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSingleSelect(commonMDListCell, mouseEvent)); + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_RELEASED, mouseEvent -> handleSingleRelease(commonMDListCell, mouseEvent)); + } + case MULTIPLE -> { + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleMultipleSelect(commonMDListCell, mouseEvent)); + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_RELEASED, mouseEvent -> handleMultipleRelease(commonMDListCell, mouseEvent)); + } + case NONE -> { + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleNoneSelect(commonMDListCell, mouseEvent)); + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_RELEASED, mouseEvent -> handleNoneRelease(commonMDListCell, mouseEvent)); + } + } + if (getSkinnable().getSelectionType() != CommonListPage.SelectionType.NONE) { + commonMDListCell.setSelectable(); + } + return commonMDListCell; + } + + private void toggleSelect(int index) { + if (listView.getSelectionModel().isSelected(index)) { + listView.getSelectionModel().clearSelection(index); + } else { + listView.getSelectionModel().select(index); + } + } + + private void handleMultipleSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isEmpty()) { + mouseEvent.consume(); + return; + } + + int currentIndex = cell.getIndex(); + if (mouseEvent.isSecondaryButtonDown()) { + requestMenu.set(true); + } else if (mouseEvent.isShiftDown()) { + if (listView.getItems().size() >= lastNotShiftClickIndex.get() && lastNotShiftClickIndex.get() >= 0) { + if (cell.isSelected()) { + IntStream.rangeClosed(Math.min(lastNotShiftClickIndex.get(), currentIndex), Math.max(lastNotShiftClickIndex.get(), currentIndex)).forEach(listView.getSelectionModel()::clearSelection); + } else { + listView.getSelectionModel().selectRange(lastNotShiftClickIndex.get(), currentIndex); + listView.getSelectionModel().select(currentIndex); + } + } else { + lastNotShiftClickIndex.set(currentIndex); + listView.getSelectionModel().select(currentIndex); + } + } else { + toggleSelect(cell.getIndex()); + lastNotShiftClickIndex.set(currentIndex); + } + cell.requestFocus(); + mouseEvent.consume(); + } + + private void handleSingleSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isEmpty()) { + mouseEvent.consume(); + return; + } + if (mouseEvent.isSecondaryButtonDown()) { + requestMenu.set(true); + } else if (cell.isSelected()) { + listView.getSelectionModel().clearSelection(); + } else { + listView.getSelectionModel().select(cell.getIndex()); + } + cell.requestFocus(); + mouseEvent.consume(); + } + + private void handleNoneSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isEmpty()) { + mouseEvent.consume(); + return; + } + + if (mouseEvent.isSecondaryButtonDown()) { + requestMenu.set(true); + } + + cell.requestFocus(); + mouseEvent.consume(); + } + + private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { + if (!requestMenu.get()) { + return; + } + + switch (getSkinnable().getCellMenuRequestSupportType()) { + case SINGLE -> getSkinnable().fireEvent(new CommonListPage.CellMenuRequestEvent<>(CommonListPage.CellMenuRequestEvent.SINGLE_CELL, cell, listView)); + case MULTIPLE -> getSkinnable().fireEvent(new CommonListPage.CellMenuRequestEvent<>(CommonListPage.CellMenuRequestEvent.MULTIPLE_CELL, cell, listView)); + case BOTH -> { + if (listView.getSelectionModel().getSelectedItems().size() > 1) { + getSkinnable().fireEvent(new CommonListPage.CellMenuRequestEvent<>(CommonListPage.CellMenuRequestEvent.MULTIPLE_CELL, cell, listView)); + } else { + getSkinnable().fireEvent(new CommonListPage.CellMenuRequestEvent<>(CommonListPage.CellMenuRequestEvent.SINGLE_CELL, cell, listView)); + } + } + } + requestMenu.set(false); + } + + private void handleSingleRelease(ListCell cell, MouseEvent mouseEvent) { + if (!requestMenu.get()) { + return; + } + getSkinnable().fireEvent(new CommonListPage.CellMenuRequestEvent<>(CommonListPage.CellMenuRequestEvent.SINGLE_CELL, cell, listView)); + } + + private void handleNoneRelease(ListCell cell, MouseEvent mouseEvent) { + handleSingleRelease(cell, mouseEvent); + } + + public static Node wrap(Node node) { + StackPane stackPane = new StackPane(node); + stackPane.setPadding(new Insets(0, 5, 0, 2)); + return stackPane; + } + + public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick, Consumer initializer) { + JFXButton ret = new JFXButton(text, wrap(svg.createIcon())); + ret.getStyleClass().add("jfx-tool-bar-button"); + ret.setOnAction(e -> onClick.run()); + if (initializer != null) { + initializer.accept(ret); + } + return ret; + } + + public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) { + return createToolbarButton(text, svg, onClick, null); + } + + public static JFXButton createToolbarButton(String text, SVG svg, BooleanProperty disableProperty, Runnable onClick) { + return createToolbarButton(text, svg, onClick, jfxButton -> jfxButton.disableProperty().bind(disableProperty)); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/CommonMDListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/CommonMDListCell.java new file mode 100644 index 0000000000..17b9875c0f --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/CommonMDListCell.java @@ -0,0 +1,39 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.construct; + +import com.jfoenix.controls.JFXListView; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventType; + +public abstract class CommonMDListCell extends MDListCell { + + public CommonMDListCell(JFXListView listView) { + super(listView); + } + + public void addCellEventHandler(EventType eventType, EventHandler eventHandler) { + getContainer().getParent().addEventHandler(eventType, eventHandler); + } + + @Override + public void setSelectable() { + super.setSelectable(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java index 49198822c2..fb6bb325ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java @@ -23,9 +23,9 @@ import org.jackhuang.hmcl.mod.Datapack; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.CommonListPage; 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; @@ -42,16 +42,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class DatapackListPage extends ListPageBase { +public final class DatapackListPage extends CommonListPage { private final Path worldDir; private final Datapack datapack; + private final ObservableList allDataPackObjects; public DatapackListPage(WorldManagePage worldManagePage) { this.worldDir = worldManagePage.getWorld().getFile(); datapack = new Datapack(worldDir.resolve("datapacks")); - setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new)); + allDataPackObjects = MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new); FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), mods -> mods.forEach(this::installSingleDatapack), this::refresh); + setSelectionType(SelectionType.MULTIPLE); refresh(); } @@ -142,4 +144,8 @@ void openDataPackFolder() { return stringPredicate.test(id) || stringPredicate.test(description); }; } + + public ObservableList getAllDataPackObjects() { + return allDataPackObjects; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java index a6698bc2e6..b1618915a8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java @@ -31,28 +31,16 @@ import javafx.collections.transformation.FilteredList; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.SelectionMode; -import javafx.scene.control.SkinBase; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.util.Duration; import org.jackhuang.hmcl.mod.Datapack; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -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.MDListCell; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.CommonMDListCell; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jetbrains.annotations.Nullable; @@ -63,71 +51,57 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.stream.IntStream; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -final class DatapackListPageSkin extends SkinBase { +final class DatapackListPageSkin extends CommonListPageSkin { - private final TransitionPane toolbarPane; private final HBox searchBar; private final HBox normalToolbar; private final HBox selectingToolbar; InvalidationListener updateBarByStateWeakListener; - private final JFXListView listView; private final FilteredList filteredList; private final BooleanProperty isSearching = new SimpleBooleanProperty(false); private final BooleanProperty isSelecting = new SimpleBooleanProperty(false); private final JFXTextField searchField; - private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1); - final Consumer toggleSelect; - DatapackListPageSkin(DatapackListPage 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"); - listView = new JFXListView<>(); - filteredList = new FilteredList<>(skinnable.getItems()); + filteredList = new FilteredList<>(skinnable.getAllDataPackObjects()); + skinnable.setItems(filteredList); + skinnable.setOnSingleCellMenuRequest((event) -> { + LOG.trace("onSingleCellMenuRequest"); + }); { - toolbarPane = new TransitionPane(); searchBar = new HBox(); normalToolbar = new HBox(); selectingToolbar = new HBox(); normalToolbar.getChildren().addAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true)) + createToolbarButton(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton(i18n("datapack.add"), SVG.ADD, skinnable::add), + createToolbarButton(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder), + createToolbarButton(i18n("search"), SVG.SEARCH, () -> isSearching.set(true)) ); selectingToolbar.getChildren().addAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + createToolbarButton(i18n("button.remove"), SVG.DELETE, () -> { Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); + skinnable.removeSelected(getSelectedItems()); }, null); }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) + createToolbarButton(i18n("mods.enable"), SVG.CHECK, () -> + skinnable.enableSelected(getSelectedItems())), + createToolbarButton(i18n("mods.disable"), SVG.CLOSE, () -> + skinnable.disableSelected(getSelectedItems())), + createToolbarButton(i18n("button.select_all"), SVG.SELECT_ALL, () -> + getListView().getSelectionModel().selectRange(0, getListView().getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker + createToolbarButton(i18n("button.cancel"), SVG.CANCEL, () -> + getListView().getSelectionModel().clearSelection()) ); searchBar.setAlignment(Pos.CENTER); @@ -141,7 +115,7 @@ final class DatapackListPageSkin extends SkinBase { pause.setRate(1); pause.playFromStart(); }); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + JFXButton closeSearchBar = createToolbarButton(null, SVG.CLOSE, () -> { isSearching.set(false); searchField.clear(); @@ -149,18 +123,8 @@ final class DatapackListPageSkin extends SkinBase { FXUtils.onEscPressed(searchField, closeSearchBar::fire); searchBar.getChildren().addAll(searchField, closeSearchBar); - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + FXUtils.onChangeAndOperate(selectedItemProperty(), selectedItem -> isSelecting.set(selectedItem != null)); - root.getContent().add(toolbarPane); updateBarByStateWeakListener = FXUtils.observeWeak(() -> { if (isSelecting.get()) { @@ -172,45 +136,19 @@ final class DatapackListPageSkin extends SkinBase { } }, isSearching, isSelecting); } + } - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.getStyleClass().add("large-spinner-pane"); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new DatapackInfoListCell(listView)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - this.listView.setItems(filteredList); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here - FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - toggleSelect = i -> { - if (listView.getSelectionModel().isSelected(i)) { - listView.getSelectionModel().clearSelection(i); - } else { - listView.getSelectionModel().select(i); - } - }; - - pane.getChildren().setAll(root); - getChildren().setAll(pane); + @Override + public CommonMDListCell listCell(JFXListView listView) { + return new DatapackInfoListCell(listView); } private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - // search button click will get focus while searchField request focus, this cause conflict. - // Defer focus request to next pulse avoids this conflict. - Platform.runLater(searchField::requestFocus); - } + setToolbar(newToolbar); + if (newToolbar == searchBar) { + // search button click will get focus while searchField request focus, this cause conflict. + // Defer focus request to next pulse avoids this conflict. + Platform.runLater(searchField::requestFocus); } } @@ -296,7 +234,7 @@ public void loadIcon(ImageView imageView, @Nullable WeakReference { + private final class DatapackInfoListCell extends CommonMDListCell { final JFXCheckBox checkBox = new JFXCheckBox(); ImageView imageView = new ImageView(); final TwoLineListItem content = new TwoLineListItem(); @@ -310,7 +248,6 @@ private final class DatapackInfoListCell extends MDListCell container.setAlignment(Pos.CENTER_LEFT); HBox.setHgrow(content, Priority.ALWAYS); content.setMouseTransparent(true); - setSelectable(); imageView.setFitWidth(32); imageView.setFitHeight(32); @@ -320,8 +257,6 @@ private final class DatapackInfoListCell extends MDListCell StackPane.setMargin(container, new Insets(8)); container.getChildren().setAll(checkBox, imageView, content); getContainer().getChildren().setAll(container); - - getContainer().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(this, mouseEvent)); } @Override @@ -336,37 +271,4 @@ protected void updateControl(DatapackInfoObject dataItem, boolean empty) { dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty())); } } - - public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) { - if (cell.isEmpty()) { - mouseEvent.consume(); - return; - } - - if (mouseEvent.isShiftDown()) { - int currentIndex = cell.getIndex(); - if (lastShiftClickIndex.get() == -1) { - lastShiftClickIndex.set(currentIndex); - toggleSelect.accept(cell.getIndex()); - } else if (listView.getItems().size() >= lastShiftClickIndex.get() && !(lastShiftClickIndex.get() < -1)) { - if (cell.isSelected()) { - IntStream.rangeClosed( - Math.min(lastShiftClickIndex.get(), currentIndex), - Math.max(lastShiftClickIndex.get(), currentIndex)) - .forEach(listView.getSelectionModel()::clearSelection); - } else { - listView.getSelectionModel().selectRange(lastShiftClickIndex.get(), currentIndex); - listView.getSelectionModel().select(currentIndex); - } - lastShiftClickIndex.set(-1); - } else { - lastShiftClickIndex.set(currentIndex); - listView.getSelectionModel().select(currentIndex); - } - } else { - toggleSelect.accept(cell.getIndex()); - } - cell.requestFocus(); - mouseEvent.consume(); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java index 02901d0687..5ee192e9df 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java @@ -21,7 +21,6 @@ import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; @@ -42,13 +41,12 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jetbrains.annotations.NotNull; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -61,7 +59,7 @@ /** * @author Glavo */ -public final class WorldBackupsPage extends ListPageBase { +public final class WorldBackupsPage extends CommonListPage { static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); private final World world; @@ -123,7 +121,7 @@ public void refresh() { @Override protected Skin createDefaultSkin() { - return new WorldBackupsPageSkin(); + return new WorldBackupsPageSkin(this); } void createBackup() { @@ -155,21 +153,15 @@ void createBackup() { }), i18n("world.backup"), null); } - private final class WorldBackupsPageSkin extends ToolbarListPageSkin { + private final class WorldBackupsPageSkin extends CommonListPageSkin { - WorldBackupsPageSkin() { - super(WorldBackupsPage.this); - } + WorldBackupsPageSkin(WorldBackupsPage skinnable) { + super(skinnable, SelectionType.NONE); - @Override - protected List initializeToolbar(WorldBackupsPage skinnable) { - JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup); - createBackup.setDisable(isReadOnly); - - return Arrays.asList( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createBackup - ); + setToolbar(new HBox( + createToolbarButton(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup, button -> button.setDisable(isReadOnly)) + )); } } @@ -221,9 +213,7 @@ private static final class BackupInfoSkin extends SkinBase { super(skinnable); World world = skinnable.getBackupWorld(); - BorderPane root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); root.setPadding(new Insets(8)); {