Hm,
First I noticed that FX will display a thumb that is the full length of
the track when there is nothing to scroll; that's unusual, most
scrollbars will hide the thumb when there is nothing to scroll in the
case where the bar is set to always visible. Perhaps something to enhance.
So I was going to say that thumb length == track length is always wrong,
but apparently in FX that's allowed. Still, if the visible portion
isn't 100% (1.0), then the thumb should never be the same length as the
track as otherwise you can't set the scrollbar's position to its minimum
and maximum positions.
So I think it's a bug. The length of the thumb should be equal to the
track length if visible portion is 1.0, otherwise it must be at least
one unit smaller in size.
This can be achieved by using the snapPortion functions instead (which
will floor values). Those haven't been exposed in SkinBase though.
I also think it may be wise to make a special case for visiblePortion =
1.0; the result of the visiblePortion multiplication, then clamp,
then snapping (which involves multiplication and division) may be
subject to some rounding and floating point errors, and it would be a
shame if the calculation results in a value that differs from
trackLength when visiblePortion == 1.0...
So, I'd change the calculation for the thumb length to something like:
thumbLength= visiblePortion == 1.0 ? trackLength :
snapPortionY(Utils.clamp(minThumbLength(), (trackLength*
visiblePortion), trackLength));
--John
On 06/10/2024 07:31, dandem sai pradeep wrote:
Hi,
In |Region| class we have the property |snapToPixel|, which is
intended to snap/round-off the pixel values of bounds for achieving
crisp user interfaces (as mentioned in the javadoc
<https://download.java.net/java/GA/javafx21.0.1/e5ab43c6aed54893b0840c1f2dcfca4d/docs/api/javafx.graphics/javafx/scene/layout/Region.html#snapToPixelProperty()>).
But is it intended/expected that, toggling this property value can
affect the core layout behavior? At-least for standard controls?
*The Problem:*
In scenarios, when the |TableRow| width is 1px greater than the
|VirtualFlow| width, the horizontal scroll bar of |VirtualFlow| is
displayed. Which is valid and I agree with that (as in below
screenshot). /In the below picture, the VirtualFlow width is 307px and
the TableRow width is 308px/.
LPH0Ukdr.png
In the above scenario, when I drag the scroll bar thumb, I would
expect a one pixel movement of the TableRow. But that is not
happening. In fact, nothing happens when I drag the thumb (neither the
content is moving nor the thumb is moving).
Upon careful investigation, it is found that the scroll bar track
width is same as thumb width, which is causing to ignore the drag event.
Below is the code of thumb drag event in ScrollBarSkin class, where
you can notice the |if| condition to skip the computing if thumb
length is not less than track length.
|thumb.setOnMouseDragged(me -> { if (me.isSynthesized()) { //
touch-screen events handled by Scroll handler me.consume(); return; }
/* ** if max isn't greater than min then there is nothing to do here
*/ if (getSkinnable().getMax() > getSkinnable().getMin()) { /* ** if
the tracklength isn't greater then do nothing.... */ if (trackLength >
thumbLength) { Point2D cur = thumb.localToParent(me.getX(),
me.getY()); if (dragStart == null) { // we're getting dragged without
getting a mouse press dragStart = thumb.localToParent(me.getX(),
me.getY()); } double dragPos = getSkinnable().getOrientation() ==
Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() -
dragStart.getX(); behavior.thumbDragged(preDragThumbPos + dragPos /
(trackLength - thumbLength)); } me.consume(); } });|
Now, when I further investigate why the |track| and |thumb| lengths
are same, I noticed that it is in fact the code of thumb length
calcuation that snaps the computed value.
|thumbLength = snapSizeX(Utils.clamp(minThumbLength(), (trackLength *
visiblePortion), trackLength));|
In the above line, the computed |trackLength| is 289.0px and the
computed |thumbLength| is 288.06168831168833px. But because this value
is snapped, the value is rounded to 289.0px which is equal to
trackLength.
*Solution / Workaround:*
From the above observation, it is clear that the snapToPixel property
of ScrollBar is impacting the computed values. So I created a custom
VirtualFlow to access the scroll bars and turn off the snapToPixel
property.
|class CustomVirtualFlow<T extends IndexedCell> extends VirtualFlow<T>
{ public CustomVirtualFlow() { getHbar().setSnapToPixel(false);
getVbar().setSnapToPixel(false); } } |
Once I included this tweak, the scroll bar is active and when I drag
the thumb, it is sliding my content. You can notice the difference in
the below gif.
pBqQPKDf.gif
So my question here is: Is this intended behavior to have
snapToPixel=true by default, which is causing to show the scrollbar
unnecessarily and it is my responsibility to turn off the snapToPixel
properties? Or in other words, is this a JavaFX bug or not?
Below is the demo code:
|import javafx.application.Application; import
javafx.beans.property.ReadOnlyObjectWrapper; import
javafx.geometry.Insets; import javafx.scene.Scene; import
javafx.scene.control.*; import
javafx.scene.control.skin.TableViewSkin; import
javafx.scene.control.skin.VirtualFlow; import
javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import
javafx.stage.Stage; import java.util.ArrayList; import java.util.List;
public class HorizontalScrollBarIssueDemo extends Application { String
CSS = "data:text/css," + """ .label{ -fx-font-weight: bold; } """;
@Override public void start(final Stage stage) throws Exception {
final VBox root = new VBox(); root.setSpacing(30); root.setPadding(new
Insets(10)); final Scene scene = new Scene(root, 500, 350);
scene.getStylesheets().add(CSS); stage.setScene(scene);
stage.setTitle("Horizontal ScrollBar Issue " +
System.getProperty("javafx.runtime.version")); stage.show(); VBox
defaultBox = new VBox(10, new Label("Default TableView"),
getTable(false)); VBox customBox = new VBox(10, new Label("TableView
whose scrollbar's snapToPixel=false"), getTable(true));
root.getChildren().addAll(defaultBox, customBox); } private
TableView<List<String>> getTable(boolean custom) {
TableView<List<String>> tableView; if (custom) { tableView = new
TableView<>() { @Override protected Skin<?> createDefaultSkin() {
return new TableViewSkin<>(this) { @Override protected
VirtualFlow<TableRow<List<String>>> createVirtualFlow() { return new
CustomVirtualFlow<>(); } }; } }; } else { tableView = new
TableView<>(); } tableView.setMaxHeight(98);
tableView.setMaxWidth(309); VBox.setVgrow(tableView, Priority.ALWAYS);
int colCount = 4; for (int i = 0; i < colCount; i++) { final int index
= i; TableColumn<List<String>, String> column = new
TableColumn<>("Option " + i); column.setCellValueFactory(param -> new
ReadOnlyObjectWrapper<>(param.getValue().get(index)));
tableView.getColumns().add(column); } for (int j = 0; j < 1; j++) {
List<String> row = new ArrayList<>(); for (int i = 0; i < colCount;
i++) { row.add("Row" + j + "-Opt" + i); }
tableView.getItems().add(row); } return tableView; } class
CustomVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
public CustomVirtualFlow() { getHbar().setSnapToPixel(false);
getVbar().setSnapToPixel(false); } } }|
Regards,
Sai Pradeep Dandem.