Skip to content

更新 JFXListView#5569

Merged
Glavo merged 17 commits intoHMCL-dev:mainfrom
Glavo:list-view
Feb 19, 2026
Merged

更新 JFXListView#5569
Glavo merged 17 commits intoHMCL-dev:mainfrom
Glavo:list-view

Conversation

@Glavo
Copy link
Copy Markdown
Member

@Glavo Glavo commented Feb 18, 2026

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the JFoenix-based JFXListView implementation used in HMCL by introducing JFXListView/JFXListCell controls and adjusting the existing skin to use the project’s FXUtils listener helper.

Changes:

  • Refactor JFXListViewSkin depth handling to use FXUtils.onChangeAndOperate.
  • Add new JFXListView control with depth, expanded/vertical-gap CSS properties, and optional tooltips.
  • Add new JFXListCell implementing ripple + expand/collapse gap animations.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java Simplifies depth listener wiring via FXUtils.onChangeAndOperate.
HMCL/src/main/java/com/jfoenix/controls/JFXListView.java New ListView control with depth + styleable expanded/vertical gap + mouse event propagation helper.
HMCL/src/main/java/com/jfoenix/controls/JFXListCell.java New ListCell implementing ripple effect, gap animations, and optional tooltips.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +201 to +241
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()));
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

Tooltips are only set when showTooltip is enabled, but they are never cleared when empty becomes true or when showTooltip is false. Because list cells are reused, stale tooltips can persist on unrelated rows. Consider calling setTooltip(null) in the empty branch and in the non-tooltip branch.

Copilot uses AI. Check for mistakes.

private void updateVerticalGap() {
if (isExpanded()) {
currentVerticalGapProperty().set(verticalGap.get());
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

updateVerticalGap() calls verticalGap.get() directly, but verticalGap may still be null if expandedProperty() is created/invalidated before verticalGapProperty() is ever accessed (e.g., when -jfx-expanded is set via CSS). This can throw an NPE. Consider reading via verticalGapProperty()/getVerticalGap() (or guarding null) to ensure the value is available.

Suggested change
currentVerticalGapProperty().set(verticalGap.get());
currentVerticalGapProperty().set(getVerticalGap());

Copilot uses AI. Check for mistakes.
public void propagateMouseEventsToParent() {
this.addEventHandler(MouseEvent.ANY, e -> {
e.consume();
this.getParent().fireEvent(e);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

propagateMouseEventsToParent() calls getParent().fireEvent(e) without checking for a null parent. If this is invoked before the control is attached to a scene graph, it will throw an NPE. Consider guarding getParent() != null and/or deferring registration until the control is added to a parent.

Suggested change
this.getParent().fireEvent(e);
if (this.getParent() != null) {
this.getParent().fireEvent(e);
}

Copilot uses AI. Check for mistakes.
///
/// 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()]
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The documentation comment has a malformed reference: method {[#makeChildrenTransparent()] is missing closing }/], which makes the comment confusing and inconsistent with other docs in this package. Please fix the bracket/brace pairing.

Suggested change
/// method {[#makeChildrenTransparent()]
/// method [#makeChildrenTransparent()]

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +93
listViewProperty().addListener((listObj, oldList, newList) -> {
if (newList instanceof JFXListView<?> listView) {
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
cellRippler.rippler.setClip(null);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

A listener is added to listView.currentVerticalGapProperty() when the cell is attached to a JFXListView, but it is never removed when the cell is detached or when listViewProperty() changes. Because the listener lambda captures the cell, this can keep cells strongly reachable from the list view and leak memory over time. Consider removing the listener from oldList, or registering it via a WeakChangeListener (e.g., FXUtils.onWeakChange).

Copilot uses AI. Check for mistakes.
getListView().requestLayout();
} else {
// fake expand state
double gap = clip.getY() * 2;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The currentVerticalGapProperty() listener uses clip.getY() to compute gap, but clip is only initialized during layoutChildren(). If the gap property changes before the first layout pass, this will throw an NPE. Consider initializing clip earlier, or computing the gap from newVal/getGap() (with null-safe handling) rather than dereferencing clip.

Suggested change
double gap = clip.getY() * 2;
double previousGap = oldVal == null ? 0.0 : oldVal.doubleValue();
double gap = previousGap * 2;

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +225
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();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

In updateItem, when newNode is a JFXRippler you bind cellRippler properties to it, but the cell is reused and these properties are never unbound. Rebinding on a later item can throw, and non-rippler items will still be affected by the old bindings. Consider unbinding before binding (and resetting to defaults) each time the graphic changes, and unbinding when newNode is not a JFXRippler.

Copilot uses AI. Check for mistakes.
cellRippler.positionProperty().bind(newRippler.positionProperty());
cellContent = newRippler.getControl();
}
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

cellContent is cast to Region unconditionally. If a list item provides a Node that isn't a Region (or a JFXRippler whose getControl() isn't a Region), this will throw ClassCastException. Consider checking instanceof Region before setting max height, or handling non-Region nodes gracefully.

Suggested change
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
if (cellContent instanceof Region region) {
region.setMaxHeight(region.prefHeight(-1));
}

Copilot uses AI. Check for mistakes.
@Glavo Glavo merged commit b6fcbb8 into HMCL-dev:main Feb 19, 2026
2 checks passed
@Glavo Glavo deleted the list-view branch February 19, 2026 12:46
Glavo added a commit to Glavo/HMCL that referenced this pull request Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants