JavaFX Slider Tick Marks Initialized via CSS Disappear

Viewed 45

I am using CSS to configure my JavaFX Sliders, then applying the style in code with:

cssSlider.getStyleClass().add("slider-style");

When I first open my window, the tick marks are present on the CSS configured Slider(s). When I close and reopen the window, the tick marks are no longer present.

This following example demonstrates the anomaly using 2 Sliders, one configured directly, the other via CSS. Click the button to hide the window for 2 seconds. Notice that the Slider in which I directly configure the attributes works fine after hiding and re-showing, but the CSS configured Slider loses its tick marks after hiding and re-showing.

Does anyone have any ideas why showing, hiding, and re-showing the window causes the tick marks to vanish from the CSS configured Slider? Am I doing something wrong, or is this a JavaFX bug?

sample.css:

    .slider-style {
        -fx-show-tick-marks: true;
        -fx-snap-to-ticks: true;
        -fx-major-tick-unit: 5;
        -fx-minor-tick-count: 5;
    }

CssExample.java:

    import javafx.application.Application;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.geometry.Insets;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.Slider;
    import javafx.scene.layout.GridPane;
    import javafx.scene.paint.Color;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    import java.net.URL;
    
    /**
     * This simple example demonstrates that JavaFX Sliders configured with CSS only show their tick marks the first time
     * they are shown.  If the Slider is hidden, then shown again, the tick marks are gone forever.
     */
    public class CssExample extends Application {
     
        @Override
        public void start(Stage stage) throws InterruptedException, IOException {
            Group root = new Group();
            Scene scene = new Scene(root, 400, 200);
            stage.setScene(scene);
            stage.setTitle("Slider Sample");
            scene.setFill(Color.BLACK);
    
            GridPane grid = new GridPane();
            grid.setPadding(new Insets(10, 10, 10, 10));
            grid.setVgap(10);
            grid.setHgap(70);
    
            scene.setRoot(grid);
            int rowNumber = 1;
    
            Label directLabel = new Label("Slider from attribute assignment");
            GridPane.setConstraints(directLabel, 1, rowNumber++);
            grid.getChildren().add(directLabel);
    
            Slider directSlider = new Slider();
            GridPane.setConstraints(directSlider, 1, rowNumber++);
            grid.getChildren().add(directSlider);
            directSlider.setShowTickMarks(true);
            directSlider.setSnapToTicks(true);
            directSlider.setMajorTickUnit(5);
            directSlider.setMinorTickCount(5);
    
            Label cssLabel = new Label("Slider from CSS (tick marks disappear after hidden)");
            GridPane.setConstraints(cssLabel, 1, rowNumber++);
            grid.getChildren().add(cssLabel);
    
            Slider cssSlider = new Slider();
            GridPane.setConstraints(cssSlider, 1, rowNumber++);
            grid.getChildren().add(cssSlider);
            URL url = getClass().getResource("sample.css");
            String cssString = url.toExternalForm();
            scene.getStylesheets().add(cssString);
            cssSlider.getStyleClass().add("slider-style");
    
            Button button = new Button("Hide for 2 Seconds");
            GridPane.setConstraints(button, 1, rowNumber++);
            grid.getChildren().add(button);
            button.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent e) {
                    stage.hide();
                    stage.show();
                }
            });
    
            stage.show();
        }
     
        public static void main(String[] args) {
            launch(args);
        }
    }
1 Answers

is this a JavaFX bug?

Yes.

See: https://github.com/openjdk/jfx/blob/fdc88341f1df8fb9c99356ada54b25124b77ea6e/modules/javafx.controls/src/main/java/javafx/scene/control/skin/SliderSkin.java#L398

It is a bug in the internal implementation of the setShowTickMarks method of SliderSkin (verified in JavaFX 18.0.1).

Test case:

import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.io.IOException;

public class CssExample extends Application {

    private static final String CSS = // language=CSS
            """
            .slider-style {
                -fx-show-tick-marks: true;
                -fx-snap-to-ticks: true;
                -fx-major-tick-unit: 5;
                -fx-minor-tick-count: 5;
            }      
            """;

    private static final String CSS_INLINE = "data:text/css," + CSS;

    @Override
    public void start(Stage stage) throws InterruptedException, IOException {
        Platform.setImplicitExit(false);

        Slider cssSlider = new Slider();
        cssSlider.showTickMarksProperty().addListener((observable, oldValue, newValue) ->
                System.out.println(cssSlider.showTickMarksProperty())
        );
        cssSlider.getStyleClass().add("slider-style");

        PauseTransition hideAnimation = new PauseTransition(Duration.seconds(2));
        hideAnimation.setOnFinished(e -> stage.show());

        Button hideWindow = new Button("Hide for 2 Seconds");
        hideWindow.setOnAction(e -> {
            stage.hide();
            hideAnimation.play();
        });

        Button closeApp = new Button("Close app");
        closeApp.setOnAction(e -> Platform.exit());

        VBox layout = new VBox(
                10,
                cssSlider, hideWindow, closeApp
        );
        layout.setPadding(new Insets(10));
        layout.setPrefSize(400, 120);

        Scene scene = new Scene(layout);
        scene.getStylesheets().add(CSS_INLINE);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Test output:

BooleanProperty [bean: Slider@132908b9[styleClass=slider slider-style], name: showTickMarks, value: true]
BooleanProperty [bean: Slider@132908b9[styleClass=slider slider-style], name: showTickMarks, value: false]
BooleanProperty [bean: Slider@132908b9[styleClass=slider slider-style], name: showTickMarks, value: true]

It switches showTicks from true to false, and back to true, which triggers the bug.

In the current implementation for the setShowTicks method:

private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
    showTickMarks = (ticksVisible || labelsVisible);
    Slider slider = getSkinnable();
    if (showTickMarks) {
        if (tickLine == null) {
            tickLine = new NumberAxis();
            tickLine.setAutoRanging(false);
            tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
            tickLine.setUpperBound(slider.getMax());
            tickLine.setLowerBound(slider.getMin());
            tickLine.setTickUnit(slider.getMajorTickUnit());
            tickLine.setTickMarkVisible(ticksVisible);
            tickLine.setTickLabelsVisible(labelsVisible);
            tickLine.setMinorTickVisible(ticksVisible);
            // add 1 to the slider minor tick count since the axis draws one
            // less minor ticks than the number given.
            tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
            if (slider.getLabelFormatter() != null) {
                tickLine.setTickLabelFormatter(stringConverterWrapper);
            }
            getChildren().clear();
            getChildren().addAll(tickLine, track, thumb);
        } else {
            tickLine.setTickLabelsVisible(labelsVisible);
            tickLine.setTickMarkVisible(ticksVisible);
            tickLine.setMinorTickVisible(ticksVisible);
        }
    }
    else  {
        getChildren().clear();
        getChildren().addAll(track, thumb);
//            tickLine = null;
    }

    getSkinnable().requestLayout();
}

The first time it shows the ticks it will do this:

getChildren().clear();
getChildren().addAll(tickLine, track, thumb);

Then, when the ticks are hidden, it will do this:

getChildren().clear();
getChildren().addAll(track, thumb);

Then, when the ticks are supposed to be shown again, the tickLine is not added back to the children, so it never shows the ticks again.

Related