2

I have the following JavaFX scene (note the setting of snapToTicks):

package com.example.javafx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;

public class SliderExample extends Application {
    public static void main(String[] args) { launch(args); }

    @Override
    public void start(Stage primaryStage) {
        Slider slider = new Slider(0.25, 2.0, 1.0);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setMajorTickUnit(0.25);
        slider.setMinorTickCount(0);

        slider.setSnapToTicks(true);    // !!!!!!!!!!

        Scene scene = new Scene(slider, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
   }
}

which renders a slider like this:

slider screenshot

Since snapToTicks is set to true the slider will finally move to the nearest value once the mouse button is released.

How can that final value be retrieved?

I tried

slider.valueProperty().addListener( n -> {
    if (!slider.isValueChanging()) {
        System.err.println(n);
    }
});

which works well except for the minimum and maximum values - if the mouse is already at a position left to the slider or at a position right to the slider, the listener will not be called at all anymore since the final value has already been set.

I have also tried to use the valueChangingProperty:

slider.valueChangingProperty().addListener( (prop, oldVal, newVal) -> {
   // NOT the final value when newVal == false!!!!!!!
   System.err.println(prop + "/" + oldVal + "/" + newVal); 
});

but the problem is that JavaFX will still change the value to the snapped value after that listener has been called with newVal equal to false (which I would even consider a bug, but probably I missed something). So its not possible to access the final, snapped value in that method.

Andreas Fester
  • 36,091
  • 7
  • 95
  • 123
  • 1
    I think adding extra conditions inside your listener on `valueProperty()` should work for you i.e. if your `slider.getValue()` is either `slider.getMin()` or `slider.getMax()`, you know that the slider has reached either side and get its value. – ItachiUchiha Feb 01 '16 at 12:21
  • looks like a bug to me: just as you, I would expect to get a change after the value has snapped – kleopatra Feb 01 '16 at 14:13
  • @ItachiUchiha Thanks - yes, I was thinking about something similar as a workaround ... However I still need to combine that with the "valueChanging" property to get the **final** value (and, thats probably unclear from the question, trigger an action when the final value, and only the final value, has been set) – Andreas Fester Feb 01 '16 at 14:14
  • @kleopatra Right :-) I think that I will wait some time to see if there is additional feedback, and if not log it as a bug ... – Andreas Fester Feb 01 '16 at 14:15
  • 1
    as to trying to get the value in the listener to isValueAdjusting: that's a variant of correlated-properties problem - unlike ol' beans, in fx there's no guarantee about the state of any other than the firing property (similar: http://stackoverflow.com/q/27186755/203657 - or https://github.com/kleopatra/swingempire-fx/wiki/Correlated-Properties ) – kleopatra Feb 01 '16 at 14:24

1 Answers1

1

I finally came up with the below solution, based on the proposal from @ItachiUchiha. Essentially, the solution uses both, a valueProperty and a valueChangingProperty listener, and uses some flags to track the current state. At the end, the perform() method is called exactly once when the slider movement is done and the final value is available. This works when the slider is moved either with the mouse or through the keyboard.

A reusable class implemented as subclass of Slider is available at https://github.com/afester/FranzXaver/blob/master/FranzXaver/src/main/java/afester/javafx/components/SnapSlider.java.

package com.example.javafx;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;

public class SliderExample extends Application {

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

    private boolean isFinal = true;     // assumption: no dragging - clicked value is the final one.
                                        // variable changes to "false" once dragging starts.

    private Double finalValue = null;

    @Override
    public void start(Stage primaryStage) {
        final Slider slider = new Slider(0.25, 2.0, 1.0);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setMajorTickUnit(0.25);
        slider.setMinorTickCount(0);
        slider.setSnapToTicks(true);

        slider.valueProperty().addListener(new ChangeListener<Number>() {

            final double minCompare = slider.getMin() + Math.ulp(slider.getMin());
            final double maxCompare = slider.getMax() - Math.ulp(slider.getMax());

            @Override
            public void changed(ObservableValue<? extends Number> observable,
                    Number oldValue, Number newValue) {

                if (isFinal) {  // either dragging of knob has stopped or
                                // no dragging was done at all (direct click or 
                                // keyboard navigation)
                    perform((Double) newValue);
                    finalValue = null;
                } else {        // dragging in progress

                    double val = (double) newValue;
                    if (val > maxCompare || val < minCompare) {
                        isFinal = true;                 // current value will be treated as final value
                                                        // once the valueChangingProperty goes to false
                        finalValue = (Double) newValue; // remember current value
                    } else {
                        isFinal = false;    // no final value anymore - slider 
                        finalValue = null;  // has been dragged to a position within 
                                            // minimum and maximum
                    }

                }
            }
        });

        slider.valueChangingProperty().addListener(new ChangeListener<Boolean>() {

            @Override
            public void changed(ObservableValue<? extends Boolean> observable,
                                Boolean oldValue, Boolean newValue) {

                if (newValue == true) { // dragging of knob started.
                    isFinal = false;    // captured values are not the final ones.

                } else {                // dragging of knob stopped.

                    if (isFinal) {      // captured value is already the final one
                                        // since it is either the minimum or the maximum value
                        perform(finalValue);
                        finalValue = null;
                    } else {
                        isFinal = true; // next captured value will be the final one
                    }
                }
            }
        });

        Scene scene = new Scene(slider, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void perform(double value) {
        System.err.printf("FINAL: %s\n", value);
    }
}
Andreas Fester
  • 36,091
  • 7
  • 95
  • 123