From e81f297f05955e5095c3e90309773156ae8fccac Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 16 Jan 2026 22:04:08 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0CommonListPage/?= =?UTF-8?q?CommonListPageSkin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/CommonListPage.java | 97 +++++++++ .../jackhuang/hmcl/ui/CommonListPageSkin.java | 180 ++++++++++++++++ .../hmcl/ui/construct/MDListCell.java | 7 + .../hmcl/ui/versions/DatapackListPage.java | 12 +- .../ui/versions/DatapackListPageSkin.java | 140 +++---------- .../hmcl/ui/versions/ModListPage.java | 16 +- .../hmcl/ui/versions/ModListPageSkin.java | 196 +++++++----------- 7 files changed, 407 insertions(+), 241 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java 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..ef3d36dab6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java @@ -0,0 +1,97 @@ +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; + + 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 static 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 + } +} 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..44bd279525 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -0,0 +1,180 @@ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; +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.ComponentList; +import org.jackhuang.hmcl.ui.construct.MDListCell; +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 lastShiftClickIndex = new AtomicInteger(-1); + private final AtomicBoolean requestMenu = new AtomicBoolean(false); + private final Consumer toggleSelect; + + public CommonListPageSkin(CommonListPage skinnable) { + super(skinnable); + + toggleSelect = i -> { + if (listView.getSelectionModel().isSelected(i)) { + listView.getSelectionModel().clearSelection(i); + } else { + listView.getSelectionModel().select(i); + } + }; + + StackPane pane = new StackPane(); + { + pane.setPadding(new Insets(10)); + getChildren().setAll(pane); + } + + ComponentList root = new ComponentList(); + { + root.getStyleClass().add("no-padding"); + pane.getChildren().setAll(root); + + root.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.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(skinnable.loadingProperty()); + center.failedReasonProperty().bind(skinnable.failedReasonProperty()); + center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); + } + { + toolbarPane.disableProperty().bind(skinnable.loadingProperty()); + } + { + // 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())); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); + center.setContent(listView); + root.getContent().addAll(toolbarPane, center); + } + } + } + + 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 getSelectedItem() { + return listView.getSelectionModel().getSelectedItems(); + } + + public ReadOnlyObjectProperty selectedItemProperty() { + return listView.getSelectionModel().selectedItemProperty(); + } + + public abstract MDListCell listCell(JFXListView listView); + + private ListCell createListCell(JFXListView listView) { + MDListCell mdListCell = listCell(listView); + mdListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(mdListCell, mouseEvent)); + mdListCell.addCellEventHandler(MouseEvent.MOUSE_RELEASED, mouseEvent -> handleRelease(mdListCell, mouseEvent)); + return mdListCell; + } + + private void handleSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isEmpty()) { + mouseEvent.consume(); + return; + } + + if (mouseEvent.isSecondaryButtonDown()) { + requestMenu.set(true); + } else 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(); + } + + private void handleRelease(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); + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java index 62fbf277a3..6f4c7eafd7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java @@ -20,6 +20,9 @@ import com.jfoenix.controls.JFXListView; import javafx.beans.binding.DoubleBinding; import javafx.css.PseudoClass; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventType; import javafx.scene.control.ListCell; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -73,5 +76,9 @@ protected void setSelectable() { }); } + public void addCellEventHandler(EventType eventType, EventHandler eventHandler) { + getContainer().getParent().addEventHandler(eventType, eventHandler); + } + protected abstract void updateControl(T item, boolean empty); } 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..fd78c8ae90 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,10 @@ 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.ui.ListPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.MappedObservableList; @@ -42,14 +43,15 @@ 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); @@ -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..f065576d1e 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 @@ -32,27 +32,16 @@ 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.*; import org.jackhuang.hmcl.ui.construct.MDListCell; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jetbrains.annotations.Nullable; @@ -63,46 +52,37 @@ 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 JFXListView listView = getListView(); 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("CellMenuRequestEvent.SINGLE_CELL received"); + if (event.getListCell() instanceof DatapackInfoListCell datapackInfoListCell) { + LOG.trace("DatapackListPageSkin::updateBarByStateWeakListener"); + } + }); { - toolbarPane = new TransitionPane(); searchBar = new HBox(); normalToolbar = new HBox(); selectingToolbar = new HBox(); @@ -117,13 +97,13 @@ final class DatapackListPageSkin extends SkinBase { selectingToolbar.getChildren().addAll( createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); + skinnable.removeSelected(getSelectedItem()); }, null); }), createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())), + skinnable.enableSelected(getSelectedItem())), createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())), + skinnable.disableSelected(getSelectedItem())), 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, () -> @@ -149,18 +129,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 +142,18 @@ 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); + public MDListCell 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); } } @@ -320,8 +263,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 +277,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(); - } -} +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 0ef9f01ded..f42e3292f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.setting.Profile; 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; @@ -49,7 +50,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class ModListPage extends ListPageBase implements VersionPage.VersionLoadable, PageAware { +public final class ModListPage extends CommonListPage implements VersionPage.VersionLoadable, PageAware { private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); private final ReentrantLock lock = new ReentrantLock(); @@ -58,6 +59,7 @@ public final class ModListPage extends ListPageBase modList; final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -114,10 +116,10 @@ private void loadMods(ModManager modManager) { updateSupportedLoaders(modManager); if (exception == null) { - getItems().setAll(list); + modList.setAll(list); } else { LOG.warning("Failed to load mods", exception); - getItems().clear(); + modList.clear(); } setLoading(false); }, Schedulers.javafx()); @@ -294,4 +296,12 @@ public Profile getProfile() { public String getInstanceId() { return this.instanceId; } + + public ObservableList getModList() { + return modList; + } + + public void setModList(ObservableList modList) { + this.modList = modList; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index c8d47870b2..eff8277b28 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -48,6 +48,7 @@ import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.CommonListPageSkin; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -79,14 +80,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -final class ModListPageSkin extends SkinBase { +final class ModListPageSkin extends CommonListPageSkin { - private final TransitionPane toolbarPane; private final HBox searchBar; private final HBox toolbarNormal; private final HBox toolbarSelecting; - private final JFXListView listView; + private final JFXListView listView = getListView(); private final JFXTextField searchField; // FXThread @@ -95,86 +95,78 @@ final class ModListPageSkin extends SkinBase { ModListPageSkin(ModListPage 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<>(); - { - toolbarPane = new TransitionPane(); - searchBar = new HBox(); - toolbarNormal = new HBox(); - toolbarSelecting = 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 -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + pause.setRate(1); + pause.playFromStart(); + }); - // Search Bar - 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 -> search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - pause.setRate(1); - pause.playFromStart(); - }); + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); - isSearching = false; - searchField.clear(); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + } - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - // Toolbar Normal - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), - createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - // Toolbar Selecting - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().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("mods.check_updates.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectAll()), - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); + toolbarNormal = new HBox(); + { + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), + createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () -> + skinnable.checkUpdates( + listView.getItems().stream() + .map(ModInfoObject::getModInfo) + .toList() + ) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + } + + toolbarSelecting = new HBox(); + { + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.removeSelected(listView.getSelectionModel().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("mods.check_updates.button"), SVG.UPDATE, () -> + skinnable.checkUpdates( + listView.getSelectionModel().getSelectedItems().stream() + .map(ModInfoObject::getModInfo) + .toList() + ) + ), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + } FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), selectedItem -> { @@ -183,68 +175,34 @@ final class ModListPageSkin extends SkinBase { else changeToolbar(toolbarSelecting); }); - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.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.getStyleClass().add("large-spinner-pane"); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new ModInfoListCell(listView)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - Bindings.bindContent(listView.getItems(), skinnable.getItems()); skinnable.getItems().addListener((ListChangeListener) c -> { if (isSearching) { search(); } }); - listView.setOnContextMenuRequested(event -> { + getSkinnable().setOnSingleCellMenuRequest(event -> { ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { listView.getSelectionModel().clearSelection(); Controllers.dialog(new ModInfoDialog(selectedItem)); } }); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it - // So we ignore it here - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); } + } - Label label = new Label(i18n("mods.not_modded")); - label.prefWidthProperty().bind(pane.widthProperty().add(-100)); - - FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> { - if (modded) pane.getChildren().setAll(root); - else pane.getChildren().setAll(label); - }); - - getChildren().setAll(pane); + @Override + public MDListCell listCell(JFXListView listView) { + return new ModInfoListCell(listView); } private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } + setToolbar(newToolbar); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); } } From 76b78c667286d63f6c003647bfd536cab6b30506 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 11:08:35 +0800 Subject: [PATCH 02/11] feat: update --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 7 +- .../hmcl/ui/versions/ModListPage.java | 16 +- .../hmcl/ui/versions/ModListPageSkin.java | 196 +++++++++++------- 3 files changed, 125 insertions(+), 94 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 44bd279525..5b35718327 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -66,6 +66,7 @@ public CommonListPageSkin(CommonListPage skinnable) { }); } { + toolbarPane.disableProperty().bind(skinnable.loadingProperty()); SpinnerPane center = new SpinnerPane(); { ComponentList.setVgrow(center, Priority.ALWAYS); @@ -73,9 +74,8 @@ public CommonListPageSkin(CommonListPage skinnable) { center.loadingProperty().bind(skinnable.loadingProperty()); center.failedReasonProperty().bind(skinnable.failedReasonProperty()); center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); - } - { - toolbarPane.disableProperty().bind(skinnable.loadingProperty()); + + root.getContent().addAll(toolbarPane, center); } { // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here @@ -84,7 +84,6 @@ public CommonListPageSkin(CommonListPage skinnable) { listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); center.setContent(listView); - root.getContent().addAll(toolbarPane, center); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index f42e3292f6..0ef9f01ded 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -31,7 +31,6 @@ import org.jackhuang.hmcl.setting.Profile; 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; @@ -50,7 +49,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class ModListPage extends CommonListPage implements VersionPage.VersionLoadable, PageAware { +public final class ModListPage extends ListPageBase implements VersionPage.VersionLoadable, PageAware { private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); private final ReentrantLock lock = new ReentrantLock(); @@ -59,7 +58,6 @@ public final class ModListPage extends CommonListPage modList; final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -116,10 +114,10 @@ private void loadMods(ModManager modManager) { updateSupportedLoaders(modManager); if (exception == null) { - modList.setAll(list); + getItems().setAll(list); } else { LOG.warning("Failed to load mods", exception); - modList.clear(); + getItems().clear(); } setLoading(false); }, Schedulers.javafx()); @@ -296,12 +294,4 @@ public Profile getProfile() { public String getInstanceId() { return this.instanceId; } - - public ObservableList getModList() { - return modList; - } - - public void setModList(ObservableList modList) { - this.modList = modList; - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index eff8277b28..c8d47870b2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -48,7 +48,6 @@ import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.CommonListPageSkin; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -80,13 +79,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -final class ModListPageSkin extends CommonListPageSkin { +final class ModListPageSkin extends SkinBase { + private final TransitionPane toolbarPane; private final HBox searchBar; private final HBox toolbarNormal; private final HBox toolbarSelecting; - private final JFXListView listView = getListView(); + private final JFXListView listView; private final JFXTextField searchField; // FXThread @@ -95,78 +95,86 @@ final class ModListPageSkin extends CommonListPageSkin search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - pause.setRate(1); - pause.playFromStart(); - }); + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + listView = new JFXListView<>(); - isSearching = false; - searchField.clear(); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); + { + toolbarPane = new TransitionPane(); - onEscPressed(searchField, closeSearchBar::fire); + searchBar = new HBox(); + toolbarNormal = new HBox(); + toolbarSelecting = new HBox(); - searchBar.getChildren().setAll(searchField, closeSearchBar); - } + // Search Bar + 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 -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + pause.setRate(1); + pause.playFromStart(); + }); - toolbarNormal = new HBox(); - { - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), - createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - } + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); - toolbarSelecting = new HBox(); - { - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().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("mods.check_updates.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectAll()), - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - } + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + // Toolbar Normal + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), + createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () -> + skinnable.checkUpdates( + listView.getItems().stream() + .map(ModInfoObject::getModInfo) + .toList() + ) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.removeSelected(listView.getSelectionModel().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("mods.check_updates.button"), SVG.UPDATE, () -> + skinnable.checkUpdates( + listView.getSelectionModel().getSelectedItems().stream() + .map(ModInfoObject::getModInfo) + .toList() + ) + ), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), selectedItem -> { @@ -175,34 +183,68 @@ final class ModListPageSkin extends CommonListPageSkin { + 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.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(skinnable.loadingProperty()); + + listView.setCellFactory(x -> new ModInfoListCell(listView)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), skinnable.getItems()); skinnable.getItems().addListener((ListChangeListener) c -> { if (isSearching) { search(); } }); - getSkinnable().setOnSingleCellMenuRequest(event -> { + listView.setOnContextMenuRequested(event -> { ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { listView.getSelectionModel().clearSelection(); Controllers.dialog(new ModInfoDialog(selectedItem)); } }); + + // ListViewBehavior would consume ESC pressed event, preventing us from handling it + // So we ignore it here + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); } - } - @Override - public MDListCell listCell(JFXListView listView) { - return new ModInfoListCell(listView); + Label label = new Label(i18n("mods.not_modded")); + label.prefWidthProperty().bind(pane.widthProperty().add(-100)); + + FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> { + if (modded) pane.getChildren().setAll(root); + else pane.getChildren().setAll(label); + }); + + getChildren().setAll(pane); } private void changeToolbar(HBox newToolbar) { - setToolbar(newToolbar); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } } } From 2995a2f2ef2cbc3db44e11a15e2bce824cc9f79a Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 11:46:05 +0800 Subject: [PATCH 03/11] feat: update --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 54 +++++++++---------- .../hmcl/ui/versions/DatapackListPage.java | 1 - .../ui/versions/DatapackListPageSkin.java | 14 ++--- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 5b35718327..53e63bfe74 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -1,7 +1,6 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.JFXListView; -import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.ObservableList; import javafx.geometry.Insets; @@ -25,12 +24,12 @@ import java.util.function.Consumer; import java.util.stream.IntStream; -public abstract class CommonListPageSkin extends SkinBase> { +public abstract class CommonListPageSkin extends SkinBase> { private final JFXListView listView = new JFXListView<>(); private final TransitionPane toolbarPane = new TransitionPane(); - private final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1); + private final AtomicInteger lastNotShiftClickIndex = new AtomicInteger(-1); private final AtomicBoolean requestMenu = new AtomicBoolean(false); private final Consumer toggleSelect; @@ -45,18 +44,18 @@ public CommonListPageSkin(CommonListPage skinnable) { } }; - StackPane pane = new StackPane(); + StackPane pagePane = new StackPane(); { - pane.setPadding(new Insets(10)); - getChildren().setAll(pane); + pagePane.setPadding(new Insets(10)); + getChildren().setAll(pagePane); } - ComponentList root = new ComponentList(); + ComponentList rootPane = new ComponentList(); { - root.getStyleClass().add("no-padding"); - pane.getChildren().setAll(root); + rootPane.getStyleClass().add("no-padding"); + pagePane.getChildren().setAll(rootPane); - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + rootPane.addEventHandler(KeyEvent.KEY_PRESSED, e -> { if (e.getCode() == KeyCode.ESCAPE) { if (listView.getSelectionModel().getSelectedItem() != null) { listView.getSelectionModel().clearSelection(); @@ -75,14 +74,14 @@ public CommonListPageSkin(CommonListPage skinnable) { center.failedReasonProperty().bind(skinnable.failedReasonProperty()); center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); - root.getContent().addAll(toolbarPane, center); + 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())); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); + this.listView.itemsProperty().bind(skinnable.itemsProperty()); center.setContent(listView); } } @@ -103,10 +102,14 @@ public JFXListView getListView() { return listView; } - public ObservableList getSelectedItem() { + public ObservableList getSelectedItems() { return listView.getSelectionModel().getSelectedItems(); } + public T getSelectedItem() { + return listView.getSelectionModel().getSelectedItems().get(0); + } + public ReadOnlyObjectProperty selectedItemProperty() { return listView.getSelectionModel().selectedItemProperty(); } @@ -126,30 +129,24 @@ private void handleSelect(ListCell cell, MouseEvent mouseEvent) { return; } + int currentIndex = cell.getIndex(); if (mouseEvent.isSecondaryButtonDown()) { requestMenu.set(true); } else 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 (listView.getItems().size() >= lastNotShiftClickIndex.get() && lastNotShiftClickIndex.get() >= 0) { if (cell.isSelected()) { - IntStream.rangeClosed( - Math.min(lastShiftClickIndex.get(), currentIndex), - Math.max(lastShiftClickIndex.get(), currentIndex)) - .forEach(listView.getSelectionModel()::clearSelection); + IntStream.rangeClosed(Math.min(lastNotShiftClickIndex.get(), currentIndex), Math.max(lastNotShiftClickIndex.get(), currentIndex)).forEach(listView.getSelectionModel()::clearSelection); } else { - listView.getSelectionModel().selectRange(lastShiftClickIndex.get(), currentIndex); + listView.getSelectionModel().selectRange(lastNotShiftClickIndex.get(), currentIndex); listView.getSelectionModel().select(currentIndex); } - lastShiftClickIndex.set(-1); } else { - lastShiftClickIndex.set(currentIndex); + lastNotShiftClickIndex.set(currentIndex); listView.getSelectionModel().select(currentIndex); } } else { toggleSelect.accept(cell.getIndex()); + lastNotShiftClickIndex.set(currentIndex); } cell.requestFocus(); mouseEvent.consume(); @@ -161,10 +158,8 @@ private void handleRelease(ListCell cell, MouseEvent mouseEvent) { } 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 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)); @@ -175,5 +170,4 @@ private void handleRelease(ListCell cell, MouseEvent mouseEvent) { } requestMenu.set(false); } - } 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 fd78c8ae90..01b0324e55 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 @@ -26,7 +26,6 @@ import org.jackhuang.hmcl.ui.CommonListPage; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.ListPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.MappedObservableList; 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 f065576d1e..89b03711d6 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,7 +31,6 @@ import javafx.collections.transformation.FilteredList; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; @@ -40,7 +39,10 @@ import javafx.util.Duration; import org.jackhuang.hmcl.mod.Datapack; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.CommonListPageSkin; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.MDListCell; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.io.CompressingUtils; @@ -97,13 +99,13 @@ final class DatapackListPageSkin extends CommonListPageSkin { Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(getSelectedItem()); + skinnable.removeSelected(getSelectedItems()); }, null); }), createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(getSelectedItem())), + skinnable.enableSelected(getSelectedItems())), createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(getSelectedItem())), + skinnable.disableSelected(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, () -> @@ -277,4 +279,4 @@ protected void updateControl(DatapackInfoObject dataItem, boolean empty) { dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty())); } } -} \ No newline at end of file +} From 5e798414091d27fc126b7f825e5720ba0cea417c Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 13:46:03 +0800 Subject: [PATCH 04/11] feat: update --- .../org/jackhuang/hmcl/ui/CommonListPage.java | 32 +++++++++ .../jackhuang/hmcl/ui/CommonListPageSkin.java | 70 ++++++++++++++++--- .../hmcl/ui/construct/CommonMDListCell.java | 39 +++++++++++ .../hmcl/ui/construct/MDListCell.java | 7 -- .../ui/versions/DatapackListPageSkin.java | 19 ++--- 5 files changed, 138 insertions(+), 29 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/CommonMDListCell.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java index ef3d36dab6..eade8b1dc6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java @@ -1,3 +1,20 @@ +/* + * 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; @@ -10,6 +27,7 @@ public class CommonListPage extends ListPageBase { private CellMenuRequestSupportType cellMenuRequestSupportType = CellMenuRequestSupportType.SINGLE; + private SelectionType selectionType = SelectionType.SINGLE; public CommonListPage() { super(); @@ -60,6 +78,14 @@ public CellMenuRequestSupportType getCellMenuRequestSupportType() { return cellMenuRequestSupportType; } + public SelectionType getSelectionType() { + return selectionType; + } + + public void setSelectionType(SelectionType selectionType) { + this.selectionType = selectionType; + } + public static class CellMenuRequestEvent extends Event { public static final EventType> ANY = @@ -94,4 +120,10 @@ public enum CellMenuRequestSupportType { 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 index 53e63bfe74..2a39ff9265 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -1,3 +1,20 @@ +/* + * 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.JFXListView; @@ -15,8 +32,8 @@ 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.MDListCell; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import java.util.concurrent.atomic.AtomicBoolean; @@ -33,8 +50,9 @@ public abstract class CommonListPageSkin extends SkinBase> private final AtomicBoolean requestMenu = new AtomicBoolean(false); private final Consumer toggleSelect; - public CommonListPageSkin(CommonListPage skinnable) { + public CommonListPageSkin(CommonListPage skinnable, CommonListPage.SelectionType selectionType) { super(skinnable); + skinnable.setSelectionType(selectionType); toggleSelect = i -> { if (listView.getSelectionModel().isSelected(i)) { @@ -80,7 +98,11 @@ public CommonListPageSkin(CommonListPage skinnable) { // 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())); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + if (skinnable.getSelectionType() == CommonListPage.SelectionType.MULTIPLE) { + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + } else { + listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + } this.listView.itemsProperty().bind(skinnable.itemsProperty()); center.setContent(listView); } @@ -114,16 +136,29 @@ public ReadOnlyObjectProperty selectedItemProperty() { return listView.getSelectionModel().selectedItemProperty(); } - public abstract MDListCell listCell(JFXListView listView); + public abstract CommonMDListCell listCell(JFXListView listView); private ListCell createListCell(JFXListView listView) { - MDListCell mdListCell = listCell(listView); - mdListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(mdListCell, mouseEvent)); - mdListCell.addCellEventHandler(MouseEvent.MOUSE_RELEASED, mouseEvent -> handleRelease(mdListCell, mouseEvent)); - return mdListCell; + CommonMDListCell commonMDListCell = listCell(listView); + switch (getSkinnable().getSelectionType()) { + case SINGLE -> { + commonMDListCell.addCellEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSingleSelect(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)); + } + } + if (getSkinnable().getSelectionType() != CommonListPage.SelectionType.NONE) { + commonMDListCell.setSelectable(); + } + return commonMDListCell; } - private void handleSelect(ListCell cell, MouseEvent mouseEvent) { + private void handleMultipleSelect(ListCell cell, MouseEvent mouseEvent) { if (cell.isEmpty()) { mouseEvent.consume(); return; @@ -152,7 +187,22 @@ private void handleSelect(ListCell cell, MouseEvent mouseEvent) { mouseEvent.consume(); } - private void handleRelease(ListCell cell, MouseEvent mouseEvent) { + private void handleSingleSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isSelected()) { + listView.getSelectionModel().clearSelection(); + } else { + listView.getSelectionModel().select(cell.getIndex()); + } + cell.requestFocus(); + mouseEvent.consume(); + } + + private void handleNoneSelect(ListCell cell, MouseEvent mouseEvent) { + cell.requestFocus(); + mouseEvent.consume(); + } + + private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { if (!requestMenu.get()) { return; } 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/construct/MDListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java index 6f4c7eafd7..62fbf277a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java @@ -20,9 +20,6 @@ import com.jfoenix.controls.JFXListView; import javafx.beans.binding.DoubleBinding; import javafx.css.PseudoClass; -import javafx.event.Event; -import javafx.event.EventHandler; -import javafx.event.EventType; import javafx.scene.control.ListCell; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -76,9 +73,5 @@ protected void setSelectable() { }); } - public void addCellEventHandler(EventType eventType, EventHandler eventHandler) { - getContainer().getParent().addEventHandler(eventType, eventHandler); - } - protected abstract void updateControl(T item, boolean empty); } 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 89b03711d6..32366fa465 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 @@ -39,11 +39,8 @@ import javafx.util.Duration; import org.jackhuang.hmcl.mod.Datapack; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.CommonListPageSkin; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.construct.MDListCell; +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; @@ -66,7 +63,6 @@ final class DatapackListPageSkin extends CommonListPageSkin listView = getListView(); private final FilteredList filteredList; private final BooleanProperty isSearching = new SimpleBooleanProperty(false); @@ -74,7 +70,7 @@ final class DatapackListPageSkin extends CommonListPageSkin(skinnable.getAllDataPackObjects()); skinnable.setItems(filteredList); skinnable.setOnSingleCellMenuRequest(event -> { @@ -107,9 +103,9 @@ final class DatapackListPageSkin extends CommonListPageSkin skinnable.disableSelected(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 + 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 createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) + getListView().getSelectionModel().clearSelection()) ); searchBar.setAlignment(Pos.CENTER); @@ -146,7 +142,7 @@ final class DatapackListPageSkin extends CommonListPageSkin listCell(JFXListView listView) { + public CommonMDListCell listCell(JFXListView listView) { return new DatapackInfoListCell(listView); } @@ -241,7 +237,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(); @@ -255,7 +251,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); From 84a273f525cf8c2332b5e21f5c71eedc65a27d4c Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 13:59:43 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E7=8E=B0=E5=9C=A8listCell?= =?UTF-8?q?=E4=BC=9A=E9=BB=98=E8=AE=A4=E4=BD=BF=E7=94=A8item=E4=B8=BAcell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 18 +++++++++++++++++- .../hmcl/ui/versions/DatapackListPageSkin.java | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 2a39ff9265..679beade86 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -136,7 +136,23 @@ public ReadOnlyObjectProperty selectedItemProperty() { return listView.getSelectionModel().selectedItemProperty(); } - public abstract CommonMDListCell listCell(JFXListView listView); + // 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) { + super.updateItem(item, empty); + if (!empty && item instanceof Node node) { + setGraphic(node); + setText(null); + } else { + setGraphic(null); + setText(null); + } + } + }; + } private ListCell createListCell(JFXListView listView) { CommonMDListCell commonMDListCell = listCell(listView); 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 32366fa465..d551475824 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 @@ -142,6 +142,7 @@ final class DatapackListPageSkin extends CommonListPageSkin listCell(JFXListView listView) { return new DatapackInfoListCell(listView); } From 4ccc8d100ea902b89f373c75cf9604cb952ba321 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 16:49:08 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84WorldBackupsPag?= =?UTF-8?q?e=EF=BC=8C=E6=B7=BB=E5=8A=A0createToolbarButton=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 45 +++++++++++++++---- .../ui/versions/DatapackListPageSkin.java | 23 +++++----- .../hmcl/ui/versions/WorldBackupsPage.java | 32 +++++-------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 679beade86..ed2993e1af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -17,7 +17,9 @@ */ 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; @@ -140,15 +142,11 @@ public ReadOnlyObjectProperty selectedItemProperty() { // 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) { - super.updateItem(item, empty); if (!empty && item instanceof Node node) { - setGraphic(node); - setText(null); - } else { - setGraphic(null); - setText(null); + getContainer().getChildren().setAll(node); } } }; @@ -224,8 +222,10 @@ private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { } 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 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)); @@ -236,4 +236,33 @@ private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { } requestMenu.set(false); } + + public static Node wrap(Node node) { + StackPane stackPane = new StackPane(); + stackPane.setPadding(new Insets(0, 5, 0, 2)); + stackPane.getChildren().setAll(node); + return stackPane; + } + + public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick, Consumer initializer) { + JFXButton ret = new JFXButton(); + ret.getStyleClass().add("jfx-tool-bar-button"); + ret.setGraphic(wrap(svg.createIcon())); + ret.setText(text); + 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/versions/DatapackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java index d551475824..d097c16f17 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 @@ -52,7 +52,6 @@ import java.nio.file.Path; import java.util.concurrent.CompletableFuture; -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; @@ -70,7 +69,7 @@ final class DatapackListPageSkin extends CommonListPageSkin(skinnable.getAllDataPackObjects()); skinnable.setItems(filteredList); skinnable.setOnSingleCellMenuRequest(event -> { @@ -86,25 +85,25 @@ final class DatapackListPageSkin extends CommonListPageSkin 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(getSelectedItems()); }, null); }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + createToolbarButton(i18n("mods.enable"), SVG.CHECK, () -> skinnable.enableSelected(getSelectedItems())), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + createToolbarButton(i18n("mods.disable"), SVG.CLOSE, () -> skinnable.disableSelected(getSelectedItems())), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + 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 - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + createToolbarButton(i18n("button.cancel"), SVG.CANCEL, () -> getListView().getSelectionModel().clearSelection()) ); @@ -119,7 +118,7 @@ final class DatapackListPageSkin extends CommonListPageSkin { isSearching.set(false); searchField.clear(); 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)); { From facd00430be089bf04c14558d42b24d956eaf895 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 21:21:59 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=E7=8E=B0=E5=9C=A8CommonListPageS?= =?UTF-8?q?kin=E7=9A=84=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=E4=B8=ADSelect?= =?UTF-8?q?ionType=E4=B8=8D=E6=98=AF=E5=BF=85=E9=A1=BB=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/CommonListPage.java | 3 +- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 38 +++++++++++-------- .../hmcl/ui/versions/DatapackListPage.java | 1 + .../ui/versions/DatapackListPageSkin.java | 8 +--- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java index eade8b1dc6..a55a2a32f5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPage.java @@ -43,7 +43,6 @@ protected void invalidated() { setEventHandler(CellMenuRequestEvent.SINGLE_CELL, get()); } }; - } return onSingleCellMenuRequest; } @@ -86,7 +85,7 @@ public void setSelectionType(SelectionType selectionType) { this.selectionType = selectionType; } - public static class CellMenuRequestEvent extends Event { + public static final class CellMenuRequestEvent extends Event { public static final EventType> ANY = new EventType<>(Event.ANY, "CELL_MENU_REQUEST"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index ed2993e1af..6e6aad7014 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -50,19 +50,19 @@ public abstract class CommonListPageSkin extends SkinBase> private final AtomicInteger lastNotShiftClickIndex = new AtomicInteger(-1); private final AtomicBoolean requestMenu = new AtomicBoolean(false); - private final Consumer toggleSelect; + + public CommonListPageSkin(CommonListPage skinnable) { + super(skinnable); + initPane(); + } public CommonListPageSkin(CommonListPage skinnable, CommonListPage.SelectionType selectionType) { super(skinnable); skinnable.setSelectionType(selectionType); + initPane(); + } - toggleSelect = i -> { - if (listView.getSelectionModel().isSelected(i)) { - listView.getSelectionModel().clearSelection(i); - } else { - listView.getSelectionModel().select(i); - } - }; + private void initPane() { StackPane pagePane = new StackPane(); { @@ -85,14 +85,14 @@ public CommonListPageSkin(CommonListPage skinnable, CommonListPage.SelectionT }); } { - toolbarPane.disableProperty().bind(skinnable.loadingProperty()); + toolbarPane.disableProperty().bind(getSkinnable().loadingProperty()); SpinnerPane center = new SpinnerPane(); { ComponentList.setVgrow(center, Priority.ALWAYS); center.getStyleClass().add("large-spinner-pane"); - center.loadingProperty().bind(skinnable.loadingProperty()); - center.failedReasonProperty().bind(skinnable.failedReasonProperty()); - center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); + center.loadingProperty().bind(getSkinnable().loadingProperty()); + center.failedReasonProperty().bind(getSkinnable().failedReasonProperty()); + center.onFailedActionProperty().bind(getSkinnable().onFailedActionProperty()); rootPane.getContent().addAll(toolbarPane, center); } @@ -100,17 +100,25 @@ public CommonListPageSkin(CommonListPage skinnable, CommonListPage.SelectionT // 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 (skinnable.getSelectionType() == CommonListPage.SelectionType.MULTIPLE) { + if (getSkinnable().getSelectionType() == CommonListPage.SelectionType.MULTIPLE) { listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } else { listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); } - this.listView.itemsProperty().bind(skinnable.itemsProperty()); + this.listView.itemsProperty().bind(getSkinnable().itemsProperty()); center.setContent(listView); } } } + private void toggleSelect(int index) { + if (listView.getSelectionModel().isSelected(index)) { + listView.getSelectionModel().clearSelection(index); + } else { + listView.getSelectionModel().select(index); + } + } + public void setToolbar(Node toolbar) { Node oldToolbar = getToolBar().getCurrentNode(); if (toolbar != oldToolbar) { @@ -194,7 +202,7 @@ private void handleMultipleSelect(ListCell cell, MouseEvent mouseEvent) { listView.getSelectionModel().select(currentIndex); } } else { - toggleSelect.accept(cell.getIndex()); + toggleSelect(cell.getIndex()); lastNotShiftClickIndex.set(currentIndex); } cell.requestFocus(); 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 01b0324e55..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 @@ -53,6 +53,7 @@ public DatapackListPage(WorldManagePage worldManagePage) { 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(); } 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 d097c16f17..acdc97f1f1 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 @@ -69,15 +69,9 @@ final class DatapackListPageSkin extends CommonListPageSkin(skinnable.getAllDataPackObjects()); skinnable.setItems(filteredList); - skinnable.setOnSingleCellMenuRequest(event -> { - LOG.trace("CellMenuRequestEvent.SINGLE_CELL received"); - if (event.getListCell() instanceof DatapackInfoListCell datapackInfoListCell) { - LOG.trace("DatapackListPageSkin::updateBarByStateWeakListener"); - } - }); { searchBar = new HBox(); From 0beb755290ae397205d8bbde4c8bcc9a7d5500df Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 17 Jan 2026 22:00:23 +0800 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 32 +++++++++++++------ .../ui/versions/DatapackListPageSkin.java | 3 ++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 6e6aad7014..a3b6fe5588 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -111,14 +111,6 @@ private void initPane() { } } - private void toggleSelect(int index) { - if (listView.getSelectionModel().isSelected(index)) { - listView.getSelectionModel().clearSelection(index); - } else { - listView.getSelectionModel().select(index); - } - } - public void setToolbar(Node toolbar) { Node oldToolbar = getToolBar().getCurrentNode(); if (toolbar != oldToolbar) { @@ -165,6 +157,7 @@ private ListCell createListCell(JFXListView 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)); @@ -180,6 +173,14 @@ private ListCell createListCell(JFXListView listView) { 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(); @@ -210,7 +211,13 @@ private void handleMultipleSelect(ListCell cell, MouseEvent mouseEvent) { } private void handleSingleSelect(ListCell cell, MouseEvent mouseEvent) { - if (cell.isSelected()) { + 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()); @@ -245,6 +252,13 @@ private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { 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)); + } + public static Node wrap(Node node) { StackPane stackPane = new StackPane(); stackPane.setPadding(new Insets(0, 5, 0, 2)); 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 acdc97f1f1..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 @@ -72,6 +72,9 @@ final class DatapackListPageSkin extends CommonListPageSkin(skinnable.getAllDataPackObjects()); skinnable.setItems(filteredList); + skinnable.setOnSingleCellMenuRequest((event) -> { + LOG.trace("onSingleCellMenuRequest"); + }); { searchBar = new HBox(); From 93e41b8c747f5eb8c0c2dc71c272b584473d9804 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 18 Jan 2026 17:34:15 +0800 Subject: [PATCH 09/11] feat: update --- .../jackhuang/hmcl/ui/CommonListPageSkin.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index a3b6fe5588..0bda418c71 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -165,6 +165,7 @@ private ListCell createListCell(JFXListView listView) { } 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) { @@ -227,6 +228,15 @@ private void handleSingleSelect(ListCell cell, MouseEvent mouseEvent) { } private void handleNoneSelect(ListCell cell, MouseEvent mouseEvent) { + if (cell.isEmpty()) { + mouseEvent.consume(); + return; + } + + if (mouseEvent.isSecondaryButtonDown()) { + requestMenu.set(true); + } + cell.requestFocus(); mouseEvent.consume(); } @@ -259,18 +269,19 @@ private void handleSingleRelease(ListCell cell, MouseEvent mouseEvent) { 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(); + StackPane stackPane = new StackPane(node); stackPane.setPadding(new Insets(0, 5, 0, 2)); - stackPane.getChildren().setAll(node); return stackPane; } public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick, Consumer initializer) { - JFXButton ret = new JFXButton(); + JFXButton ret = new JFXButton(text, wrap(svg.createIcon())); ret.getStyleClass().add("jfx-tool-bar-button"); - ret.setGraphic(wrap(svg.createIcon())); - ret.setText(text); ret.setOnAction(e -> onClick.run()); if (initializer != null) { initializer.accept(ret); @@ -283,8 +294,6 @@ public static JFXButton createToolbarButton(String text, SVG svg, Runnable onCli } public static JFXButton createToolbarButton(String text, SVG svg, BooleanProperty disableProperty, Runnable onClick) { - return createToolbarButton(text, svg, onClick, jfxButton -> { - jfxButton.disableProperty().bind(disableProperty); - }); + return createToolbarButton(text, svg, onClick, jfxButton -> jfxButton.disableProperty().bind(disableProperty)); } } From 9ed3923d2060498aa6375e7c1af955666bb49e6c Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 18 Jan 2026 17:58:33 +0800 Subject: [PATCH 10/11] feat: update --- .../main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 0bda418c71..5780085918 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -109,6 +109,9 @@ private void initPane() { center.setContent(listView); } } + + FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), (newValue) -> lastNotShiftClickIndex.set(-1)); + FXUtils.onChangeAndOperate(getSkinnable().itemsProperty(), (newValue) -> lastNotShiftClickIndex.set(-1)); } public void setToolbar(Node toolbar) { From ffc21d79dadb4f10597388b28fb96bce95fca1a3 Mon Sep 17 00:00:00 2001 From: mine_ Date: Mon, 19 Jan 2026 21:41:19 +0800 Subject: [PATCH 11/11] fix: style --- .../main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java index 5780085918..3906693098 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CommonListPageSkin.java @@ -250,10 +250,8 @@ private void handleMultipleRelease(ListCell cell, MouseEvent mouseEvent) { } 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 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));