Conversation
There was a problem hiding this comment.
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
JFXListViewSkindepth handling to useFXUtils.onChangeAndOperate. - Add new
JFXListViewcontrol with depth, expanded/vertical-gap CSS properties, and optional tooltips. - Add new
JFXListCellimplementing 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.
| 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())); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
|
|
||
| private void updateVerticalGap() { | ||
| if (isExpanded()) { | ||
| currentVerticalGapProperty().set(verticalGap.get()); |
There was a problem hiding this comment.
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.
| currentVerticalGapProperty().set(verticalGap.get()); | |
| currentVerticalGapProperty().set(getVerticalGap()); |
| public void propagateMouseEventsToParent() { | ||
| this.addEventHandler(MouseEvent.ANY, e -> { | ||
| e.consume(); | ||
| this.getParent().fireEvent(e); |
There was a problem hiding this comment.
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.
| this.getParent().fireEvent(e); | |
| if (this.getParent() != null) { | |
| this.getParent().fireEvent(e); | |
| } |
| /// | ||
| /// 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()] |
There was a problem hiding this comment.
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.
| /// method {[#makeChildrenTransparent()] | |
| /// method [#makeChildrenTransparent()] |
| listViewProperty().addListener((listObj, oldList, newList) -> { | ||
| if (newList instanceof JFXListView<?> listView) { | ||
| listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> { | ||
| cellRippler.rippler.setClip(null); |
There was a problem hiding this comment.
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).
| getListView().requestLayout(); | ||
| } else { | ||
| // fake expand state | ||
| double gap = clip.getY() * 2; |
There was a problem hiding this comment.
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.
| double gap = clip.getY() * 2; | |
| double previousGap = oldVal == null ? 0.0 : oldVal.doubleValue(); | |
| double gap = previousGap * 2; |
| 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(); |
There was a problem hiding this comment.
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.
| cellRippler.positionProperty().bind(newRippler.positionProperty()); | ||
| cellContent = newRippler.getControl(); | ||
| } | ||
| ((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1)); |
There was a problem hiding this comment.
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.
| ((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1)); | |
| if (cellContent instanceof Region region) { | |
| region.setMaxHeight(region.prefHeight(-1)); | |
| } |
No description provided.