From b6fcbb831cdbe3f3b812da599da1813d28771978 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 20:45:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20JFXListView=20(#5569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jfoenix/controls/JFXListCell.java | 271 ++++++++++++++++++ .../com/jfoenix/controls/JFXListView.java | 258 +++++++++++++++++ .../com/jfoenix/skins/JFXListViewSkin.java | 3 +- 3 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXListCell.java create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXListView.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXListCell.java b/HMCL/src/main/java/com/jfoenix/controls/JFXListCell.java new file mode 100644 index 000000000..13b1eadae --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXListCell.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.utils.JFXNodeUtils; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Shape; +import javafx.util.Duration; + +import java.util.Set; + +/// material design implementation of ListCell +/// +/// By default, JFXListCell will try to create a graphic node for the cell, +/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method. +/// +/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to +/// show the ripple effect upon clicking , to change this behavior you can override the +/// method {[#makeChildrenTransparent()] +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXListCell extends ListCell { + + protected JFXRippler cellRippler = new JFXRippler(this) { + @Override + protected Node getMask() { + Region clip = new Region(); + JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip); + double width = control.getLayoutBounds().getWidth(); + double height = control.getLayoutBounds().getHeight(); + clip.resize(width, height); + return clip; + } + + @Override + protected void positionControl(Node control) { + // do nothing + } + }; + + protected Node cellContent; + private Rectangle clip; + + private Timeline gapAnimation; + private boolean playExpandAnimation = false; + private boolean selectionChanged = false; + + /** + * {@inheritDoc} + */ + public JFXListCell() { + initialize(); + initListeners(); + } + + /** + * init listeners to update the vertical gap / selection animation + */ + private void initListeners() { + listViewProperty().addListener((listObj, oldList, newList) -> { + if (newList instanceof JFXListView listView) { + listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> { + cellRippler.rippler.setClip(null); + if (newVal.doubleValue() != 0) { + playExpandAnimation = true; + getListView().requestLayout(); + } else { + // fake expand state + double gap = clip.getY() * 2; + gapAnimation = new Timeline( + new KeyFrame(Duration.millis(240), + new KeyValue(this.translateYProperty(), + -gap / 2 - (gap * (getIndex())), + Interpolator.EASE_BOTH) + )); + gapAnimation.play(); + gapAnimation.setOnFinished((finish) -> { + requestLayout(); + Platform.runLater(() -> getListView().requestLayout()); + }); + } + }); + + selectedProperty().addListener((o, oldVal, newVal) -> { + if (newVal) { + selectionChanged = true; + } + }); + } + }); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + cellRippler.resizeRelocate(0, 0, getWidth(), getHeight()); + double gap = getGap(); + + if (clip == null) { + clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap); + setClip(clip); + } else { + if (gap != 0) { + if (playExpandAnimation || selectionChanged) { + // fake list collapse state + if (playExpandAnimation) { + this.setTranslateY(-gap / 2 + (-gap * (getIndex()))); + clip.setY(gap / 2); + clip.setHeight(getHeight() - gap); + gapAnimation = new Timeline(new KeyFrame(Duration.millis(240), + new KeyValue(this.translateYProperty(), + 0, + Interpolator.EASE_BOTH))); + playExpandAnimation = false; + } else if (selectionChanged) { + clip.setY(0); + clip.setHeight(getHeight()); + gapAnimation = new Timeline( + new KeyFrame(Duration.millis(240), + new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH), + new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH) + )); + } + playExpandAnimation = false; + selectionChanged = false; + gapAnimation.play(); + } else { + if (gapAnimation != null) { + gapAnimation.stop(); + } + this.setTranslateY(0); + clip.setY(gap / 2); + clip.setHeight(getHeight() - gap); + } + } else { + this.setTranslateY(0); + clip.setY(0); + clip.setHeight(getHeight()); + } + clip.setX(0); + clip.setWidth(getWidth()); + } + if (!getChildren().contains(cellRippler)) { + makeChildrenTransparent(); + getChildren().add(0, cellRippler); + cellRippler.rippler.clear(); + } + } + + /** + * this method is used to set some nodes in cell content as mouse transparent nodes + * so clicking on them will trigger the ripple effect. + */ + protected void makeChildrenTransparent() { + for (Node child : getChildren()) { + if (child instanceof Label) { + Set texts = child.lookupAll("Text"); + for (Node text : texts) { + text.setMouseTransparent(true); + } + } else if (child instanceof Shape) { + child.setMouseTransparent(true); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + setText(null); + setGraphic(null); + // remove empty (Trailing cells) + setMouseTransparent(true); + setStyle("-fx-background-color:TRANSPARENT;"); + } else { + setMouseTransparent(false); + setStyle(null); + if (item instanceof Node newNode) { + setText(null); + Node currentNode = getGraphic(); + if (currentNode == null || !currentNode.equals(newNode)) { + cellContent = newNode; + cellRippler.rippler.cacheRippleClip(false); + // build the Cell node + // RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties + if (newNode instanceof JFXRippler newRippler) { + // build cell container from exisiting rippler + cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty()); + cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty()); + cellRippler.positionProperty().bind(newRippler.positionProperty()); + cellContent = newRippler.getControl(); + } + ((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1)); + setGraphic(cellContent); + } + } else { + setText(item == null ? "null" : item.toString()); + setGraphic(null); + } + // show cell tooltip if it's toggled in JFXListView + if (getListView() instanceof JFXListView listView && listView.isShowTooltip()) { + if (item instanceof Label label) { + setTooltip(new Tooltip(label.getText())); + } else if (getText() != null) { + setTooltip(new Tooltip(getText())); + } + } + } + } + + // Stylesheet Handling * + + /** + * Initialize the style class to 'jfx-list-cell'. + *

+ * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell"; + + private void initialize() { + this.getStyleClass().add(DEFAULT_STYLE_CLASS); + this.setPadding(new Insets(8, 12, 8, 12)); + } + + @Override + protected double computePrefHeight(double width) { + double gap = getGap(); + return super.computePrefHeight(width) + gap; + } + + private double getGap() { + return (getListView() instanceof JFXListView listView) + ? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0) + : 0; + } +} diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXListView.java b/HMCL/src/main/java/com/jfoenix/controls/JFXListView.java new file mode 100644 index 000000000..d02d6d660 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXListView.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.skins.JFXListViewSkin; +import javafx.beans.property.*; +import javafx.css.*; +import javafx.css.converter.BooleanConverter; +import javafx.css.converter.SizeConverter; +import javafx.scene.control.ListView; +import javafx.scene.control.Skin; +import javafx.scene.input.MouseEvent; + +import java.util.*; + +/// Material design implementation of List View +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXListView extends ListView { + + /** + * {@inheritDoc} + */ + public JFXListView() { + this.setCellFactory(listView -> new JFXListCell<>()); + initialize(); + } + + /** + * {@inheritDoc} + */ + @Override + protected Skin createDefaultSkin() { + return new JFXListViewSkin<>(this); + } + + private IntegerProperty depth; + + public IntegerProperty depthProperty() { + if (depth == null) { + depth = new SimpleIntegerProperty(this, "depth", 0); + } + return depth; + } + + public int getDepth() { + return depth != null ? depth.get() : 0; + } + + public void setDepth(int depth) { + depthProperty().set(depth); + } + + private DoubleProperty currentVerticalGap; + + DoubleProperty currentVerticalGapProperty() { + if (currentVerticalGap == null) { + currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap"); + } + return currentVerticalGap; + } + + private void updateVerticalGap() { + if (isExpanded()) { + currentVerticalGapProperty().set(verticalGap.get()); + } else { + currentVerticalGapProperty().set(0); + } + } + + /* + * this only works if the items were labels / strings + */ + private BooleanProperty showTooltip; + + public final BooleanProperty showTooltipProperty() { + if (showTooltip == null) { + showTooltip = new SimpleBooleanProperty(this, "showTooltip", false); + } + return this.showTooltip; + } + + public final boolean isShowTooltip() { + return showTooltip != null && showTooltip.get(); + } + + public final void setShowTooltip(final boolean showTooltip) { + this.showTooltipProperty().set(showTooltip); + } + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /// Initialize the style class to 'jfx-list-view'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-list-view"; + + private void initialize() { + this.getStyleClass().add(DEFAULT_STYLE_CLASS); + } + + /** + * propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list) + */ + public void propagateMouseEventsToParent() { + this.addEventHandler(MouseEvent.ANY, e -> { + e.consume(); + this.getParent().fireEvent(e); + }); + } + + private StyleableDoubleProperty verticalGap; + + public StyleableDoubleProperty verticalGapProperty() { + if (this.verticalGap == null) { + this.verticalGap = new StyleableDoubleProperty(0.0) { + @Override + public Object getBean() { + return JFXListView.this; + } + + @Override + public String getName() { + return "verticalGap"; + } + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.VERTICAL_GAP; + } + + @Override + protected void invalidated() { + updateVerticalGap(); + } + }; + } + return this.verticalGap; + } + + public Double getVerticalGap() { + return verticalGap == null ? 0.0 : verticalGap.get(); + } + + public void setVerticalGap(Double gap) { + verticalGapProperty().set(gap); + } + + private StyleableBooleanProperty expanded; + + public StyleableBooleanProperty expandedProperty() { + if (expanded == null) { + expanded = new StyleableBooleanProperty(false) { + @Override + public Object getBean() { + return JFXListView.this; + } + + @Override + public String getName() { + return "expanded"; + } + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.EXPANDED; + } + + @Override + protected void invalidated() { + updateVerticalGap(); + } + }; + } + + return this.expanded; + } + + public Boolean isExpanded() { + return expanded != null && expanded.get(); + } + + public void setExpanded(Boolean expanded) { + expandedProperty().set(expanded); + } + + private static final class StyleableProperties { + private static final CssMetaData, Number> VERTICAL_GAP = + new CssMetaData<>("-jfx-vertical-gap", + SizeConverter.getInstance(), 0) { + @Override + public boolean isSettable(JFXListView control) { + return control.verticalGap == null || !control.verticalGap.isBound(); + } + + @Override + public StyleableDoubleProperty getStyleableProperty(JFXListView control) { + return control.verticalGapProperty(); + } + }; + private static final CssMetaData, Boolean> EXPANDED = + new CssMetaData<>("-jfx-expanded", + BooleanConverter.getInstance(), false) { + @Override + public boolean isSettable(JFXListView control) { + // it's only settable if the List is not shown yet + return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound()); + } + + @Override + public StyleableBooleanProperty getStyleableProperty(JFXListView control) { + return control.expandedProperty(); + } + }; + private static final List> CHILD_STYLEABLES; + + static { + final List> styleables = + new ArrayList<>(ListView.getClassCssMetaData()); + Collections.addAll(styleables, VERTICAL_GAP, EXPANDED); + CHILD_STYLEABLES = List.copyOf(styleables); + } + } + + @Override + public List> getControlCssMetaData() { + return getClassCssMetaData(); + } + + public static List> getClassCssMetaData() { + return StyleableProperties.CHILD_STYLEABLES; + } + +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java index 4f8f0624d..f7eaf5ea7 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java @@ -32,8 +32,7 @@ public class JFXListViewSkin extends ListViewSkin { public JFXListViewSkin(final JFXListView listView) { super(listView); VirtualFlow> flow = getVirtualFlow(); - JFXDepthManager.setDepth(flow, listView.depthProperty().get()); - listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal)); + FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue())); if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) { FXUtils.smoothScrolling(flow);