How to maintain ratio in values of JavaFX Spinner - java

I have two spinners and a toggle button in my JavaFX application, and I've also set them editable
Spinner<Integer> hSelector = new Spinner<>(1,15000,1000,1);
Spinner<Integer> wSelector = new Spinner<>(1,15000,1000,1);
hSelector.setEditable(true);
wSelector.setEditable(true);
ToggleButton lock = new ToggleButton("lock");
When this lock toggle button is selected I want to maintain the ratio in values of hSelector and wSelector. So I tried following,
lock.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue){
hSelector.getValueFactory().valueProperty().addListener((ob, ov, nv) -> {
wSelector.getEditor().setText(String.valueOf((nv * wSelector.getValue()) / ov));
});
wSelector.getValueFactory().valueProperty().addListener((ob, ov, nv) -> {
hSelector.getEditor().setText(String.valueOf((nv * hSelector.getValue()) / ov));
});
}
});
But this did not work well. The problem is that when lock is selected and I change the value of one spinner and then I just focus(by clicking in text field of spinner) them both one by one, their value changes automatically even when I don't edit the values.
Now, my questions, What is the correct way to maintain the ratio in values of these spinners?, what is the problem in my method? and how to remove the behavior of maintaining ratio after lock is un-selected.

Do not add a new listener every time the toggle button changes. “Add listener” means the method literally adds a listener to the existing set of listeners, so as your code is written, if the user were to select the toggle button ten times, you would have ten listeners on each spinner.
The correct approach is to add one listener to each Spinner. Each listener should do nothing if the toggle button is not selected. You will want to keep the spinner values’ ratio in a private field, for the listeners to use.
private double ratio = 1;
// ...
lock.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
hSelector.commitValue();
wSelector.commitValue();
ratio = (double) hSelector.getValue() / wSelector.getValue();
}
});
hSelector.getValueFactory().valueProperty().addListener((ob, ov, nv) -> {
if (lock.isSelected()) {
wSelector.getValueFactory().setValue((int) (nv / ratio));
}
});
wSelector.getValueFactory().valueProperty().addListener((ob, ov, nv) -> {
if (lock.isSelected()) {
hSelector.getValueFactory().setValue((int) (nv * ratio));
}
});

You do not want to add a listener to the selectedProperty. Instead, listen for changes to the Spinner values. When the value changes, check if the lock button has been selected and perform your calculations then:
hSelector.valueProperty().addListener((observable, oldValue, newValue) -> {
if (lock.isSelected()) {
wSelector.getValueFactory().setValue(newValue * wSelector.getValue() / oldValue);
}
});
wSelector.valueProperty().addListener((observable, oldValue, newValue) -> {
if (lock.isSelected()) {
hSelector.getValueFactory().setValue(newValue * hSelector.getValue() / oldValue);
}
});
You may need to check the formula you're using to determine the new ratio, however, as it does not seem to work properly; mathematics is not my strong-suit. But this should get your spinners to update properly when changing one's values.

Related

Java JSpinner increase value with down arrow

I have a JSpinner that has all integers as model. I want it's value to increase with the down arrow and value to decrease with up arrow,which is the exact opposite of the default usage.
I have already done this using a variable that has the previous value and added a Change Listener. Whenever a value changes i compare it to the previous. If it's about to increase i instead decrease it and vice versa.
I wanted to know if there is another way of doing it. Like a listener who knows which arrow is pressed.
SpinnerNumberModel model = new SpinnerNumberModel(5, 1, 100, -1); // Note: -ve step
Andrew's answer is the way to go. This is another way to achieve it via a ChangeListener, since you mentioned it.
A ChangeListener fires every time the value changes in the spinner field.
The steps are:
Keep a field reference of its current value.
Detect the new value in listener
If the new value is higher than the old value, it means the ↑ arrow pressed
Otherwise, it means the ↓ arrow pressed
According to the change, set the opposite value
An SSCCE of what I mean:
public class SpinnerExample extends JFrame {
private int oldValue;
public SpinnerExample() {
super("");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
JSpinner spinner = new JSpinner();
spinner.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
int newValue = (int) spinner.getValue();
spinner.removeChangeListener(this);
if (newValue > oldValue) //next value button pressed
spinner.setValue(oldValue - (newValue - oldValue));
else
spinner.setValue(oldValue + (oldValue - newValue));
oldValue = (int) spinner.getValue();
spinner.addChangeListener(this);
}
});
add(spinner);
setLocationByPlatform(true);
pack();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SpinnerExample().setVisible(true));
}
}
Note that the listener is removed before changing the value and after the change is added again. If you dont remove-change-add the listener, you will end up with a StackOverFlow error since changing the value will fire the listener again and again.
Also, remember that this in line spinner.removeChangeListener(this); refers to the ChangeListener. Converting this listener to a lambda expression:
spinner.addChangeListener(e -> {
int newValue = (int) spinner.getValue();
spinner.removeChangeListener(this);
if (newValue > oldValue) //next value button pressed
spinner.setValue(oldValue - (newValue - oldValue));
else
spinner.setValue(oldValue + (oldValue - newValue));
oldValue = (int) spinner.getValue();
System.out.println("oldValue:" + oldValue);
spinner.addChangeListener(this);
});
you will not able to remove the listener, since the this will be a reference to SpinnerExample instance and not the listener. Long story short, keep it an anonymous class and not a lambda.
Another way to accomplish is to switch the handlers/listeners of these buttons (probably reflection will have to interfere), but it will require a lot more effort. Plus there is no guarantee that will work for other look and feels, since they are initiated in <LookAndFeel>-ComboBoxUI.

On Javafx, how do I keep the ratio of the window(entire stage) when I change its width or height by click-and-dragging? [duplicate]

I want to perform some functionality on resize event of form (or Scene or Stage whatever it is).
But how can I detect resize event of form in JavaFX?
You can listen to the changes of the widthProperty and the heightProperty of the Stage:
stage.widthProperty().addListener((obs, oldVal, newVal) -> {
// Do whatever you want
});
stage.heightProperty().addListener((obs, oldVal, newVal) -> {
// Do whatever you want
});
Note: To listen to both width and height changes, the same listener can be used really simply:
ChangeListener<Number> stageSizeListener = (observable, oldValue, newValue) ->
System.out.println("Height: " + stage.getHeight() + " Width: " + stage.getWidth());
stage.widthProperty().addListener(stageSizeListener);
stage.heightProperty().addListener(stageSizeListener);
Keeping a fixed width to height ratio:
stage.minHeightProperty().bind(stage.widthProperty().multiply(0.5));
stage.maxHeightProperty().bind(stage.widthProperty().multiply(0.5));
Cut the long story short :
container.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.floatValue()!=oldValue.floatValue()) resizeKids(newValue);
}
});
Also you may want to check new width/height with the old values for prevent duplication.

JavaFX addListener not working

I'm quite a newbie on JavaFX and I need to bind the visible property of a Label in a way that, if the value it represents reaches 0, the Label should be invisible. Also, it needs to be updated when the bounded integerProperty value changes.
This is my code:
#FXML
private Label kingRewardLabel;
// many other stuff between
IntegerProperty kingBonus = mainApp.getLocalModel().getMap().kingBonus();
kingBonus.addListener((observable, oldValue, newValue) -> {
if (newValue.equals(0)) {
kingRewardLabel.setVisible(false);
} else {
kingRewardLabel.setText(String.valueOf(newValue.intValue()));
}
});
// testing the listener
kingBonus.setValue(25);
I have already tried to debug a little but everything seems fine, no error, no exception thrown, just the listener does not work, or at least not as I expect, because the Label still show the default text "Label", instead of "25"
You can use simply bindings to achieve this:
kingRewardLabel.textProperty().bind(kingBonus.asString());
kingRewardLabel.visibleProperty().bind(kingBonus.greaterThan(0));
The Label kingRewardLabel will display the value of the IntegerProperty kingBonus and it is only visible if the displayed value is greater than zero.
But, if you want to stay with listeners:
kingBonus.addListener((obs, oldVal, newVal) -> {
kingRewardLabel.setVisible(newVal.intValue() > 0);
kingRewardLabel.setText(newVal.toString());
});
This is almost the same as your listener in the question, but in that case, if the Label became invisible, it will never become visible again as kingRewardLabel.setVisible(true) is never called.
Finally, to answer your question about why the listener is "not working" - there can be two possible reasons:
1) The Label, which one is displayed is not the Label stored in kingRewardLabel
2) At the time when you call kingBonus.setValue(25);, the value stored in kingBonus is already 25, no changed event will be fired, therefore the listener is not executed at all.
You can go like this:
kingBonus.addListener(l -> {
int value = kingBonus.getValue().intValue();
System.out.println("Entered listener for value:" + value);
if (value == 0)
kingRewardLabel.setVisible(false);
else
kingRewardLabel.setText(value+"");
});
});

How to set a slider to fix to positions?

I have a slider and I require it to fix to int numbers 1-23. So if the user were to move it from 1 they would end up on any number that is 1-23, unlike a double value. The purpose of this slider is to be a clock, you drag it and different times appear.
My first attempt:
private final int MIN_CHANGE = 1;
....
timeSlider.setMax(23);
....
timeSlider.valueProperty().addListener((obs, oldValue, newValue) -> {
if (!timeSlider.isValueChanging()) {
if (Math.abs(newValue.intValue()) > MIN_CHANGE) {
timeSlider.setValue(newValue.intValue());
}
}
});
This more or less works but only for clicking, not for dragging.
How could I get a slider to fix to whole numbers like 1-23 when dragging if the getValue is a double?
Another problem I face is when dragging the pm/am changing is not sudden.
timeSlider.valueChangingProperty().addListener((obs, wasChanging, isChanging) -> {
if (isChanging) {
if(timeSlider.getValue() < 12.99) {
labelTest.textProperty().bind(Bindings.format("%.0f:00am", timeSlider.valueProperty()));
} else {
labelTest.textProperty().bind(Bindings.format("%.0f:00pm", timeSlider.valueProperty()));
}
}
});
Try settings snapToTicks to true and the major tick spacing to 23:
timeSlider.setSnapToTicks(true);
timeSlider.setMajorTickUnit(23.0);
timeSlider.setBlockIncrement(23.0);
timeSlider.setMinorTickCount(0.0); // Disable minor ticks
In order to make it snap to numbers I used the blockIncrement, majorTickUnit and snapToTicks provided in the FXML scene builder which I didn't notice before until now, thanks to ItachiUchiha.
For reference I used the following settings
min="1.0"
max="23.0"
blockIncrement="1.0"
majorTickUnit="1.0"
minorTickCount="0"
showTickMarks="true"
snapToTicks="true"
Lastly, to fix my issue with am/pm not updating I changed my method for am/pm binding to the following
timeSlider.valueProperty().addListener((obs, oldValue, newValue) -> {
if (timeSlider.isValueChanging()) {
if (newValue.intValue() < 12) {
labelTest.textProperty().bind(Bindings.format("%.0f:00am", timeSlider.valueProperty()));
} else {
labelTest.textProperty().bind(Bindings.format("%.0f:00pm", timeSlider.valueProperty()));
}
}
});

Javafx Slider event listener being called before it snaps to the nearest tick

I am trying to update some information every time the Slider value is changed, so I have the code set up like this:
int betAmount = 0;
Slider betSlider = new Slider();
betSlider.setMinorTickCount(4);
betSlider.setMajorTickUnit(250);
betSlider.setSnapToTicks(true);
betSlider.valueChangingProperty().addListener((obs, wasChanging, isChanging) -> {
if (!isChanging) {
betAmount = (int) betSlider.getValue();
update(); //update method, not relevant to problem
}
});
The problem that I am having is that the getValue() method on my slider is being called before it snaps to the nearest tick. Because of this, I am getting the incorrect value stored in my betAmount variable. I was wondering if there was any way to get the slider's value after it has finished snapping to the nearest tick.
Try using valueProperty() in place of valueChangingProperty()
betSlider.valueProperty().addListener((obs, oldValue, newValue) -> {
betAmount = newValue.intValue());
});
valueChanging -> It provides notification that the value is changing.
value -> The current value represented by this Slider.
The way I've done this in the past is to use the slider's valueProperty() and filter out any new values that occur while the slider is still changing.
int betAmount = 0;
Slider betSlider = new Slider();
betSlider.setMinorTickCount(4);
betSlider.setMajorTickUnit(250);
betSlider.setSnapToTicks(true);
betSlider.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue != null && !newValue.equals(oldValue) && !betSlider.isValueChanging()) {
betAmount = newValue.intValue();
update(); //update method, not relevant to problem
}
});

Categories

Resources