Reputation: 2938
Imagine having an enum defining mouse-modes:
public enum MouseMode {
SELECTION,
EDITING,
DELETING }
And imagine having a toggle-group made of 3 buttons:
ToggleButton selection = new ToggleButton("Select");
ToggleButton editing = new ToggleButton("Edit");
ToggleButton deleting = new ToggleButton("Delete");
ToggleGroup mouseSelection = new ToggleGroup();
I want a field MouseMode currentMode
to be bidirectionally linked to the toggle-group. Whenever a toggle is set, currentMode is switched accordingly but also if some external process changes currentMode (maybe a key press) then the togglegroup adapts accordingly.
I can do this with 2 listeners but I wonder if there is a way to create a custom bidirectional map.
Upvotes: 9
Views: 6786
Reputation: 391
I'm a bit late to the party, but here is a naive and dependency-less way to wrap the toggle group's selectedToggle property in something that can be bound bidirectionally
public static SimpleObjectProperty<Toggle> wrapToggleGroupSelectedProperty(ToggleGroup tg) {
SimpleObjectProperty<Toggle> bidir = new SimpleObjectProperty<>(tg.getSelectedToggle()) {
@Override
public Toggle get() {
Toggle superGet = super.get();
if(tg.selectedToggleProperty().get() != superGet)
set(superGet = tg.selectedToggleProperty().get());
return superGet;
}
@Override
public void set(Toggle newValue) {
if(tg.selectedToggleProperty().get() != newValue)
tg.selectToggle(newValue);
super.set(newValue);
}
};
tg.selectedToggleProperty().addListener((s,a,b)-> {
tg.selectedToggleProperty().get();
bidir.get();
});
return bidir;
}
Similarly you can simply wrap the property with a mapping in mind like so
public static SimpleObjectProperty<MouseMode> wrapToggleGroupSelectedProperty(ToggleGroup tg) {
SimpleObjectProperty<MouseMode> bidir = new SimpleObjectProperty<>() {
@Override
public MouseMode get() {
MouseMode superGet = super.get();
MouseMode selectedToggle = map(tg.selectedToggleProperty().get());
if(selectedToggle != superGet)
set(superGet = selectedToggle;
return superGet;
}
@Override
public void set(MouseMode newValue) {
Toggle mapped = map(newValue);
if(tg.selectedToggleProperty().get() != mapped)
tg.selectToggle(mapped);
super.set(newValue);
}
private Toggle map(MouseMode mode) {
return switch(mode) {
case SELECTION: return toggleA;
case EDITING: return toggleB;
case DELETING: return toggleC;
};
}
private MouseMode map(Toggle toggle) {
if(toggle == toggleA)
return MouseMode.SELECTION;
else if(toggle == toggleB)
return MouseMode.EDITING;
else if(toggle == toggleC)
return MouseMode.DELETING;
return null;
}
};
tg.selectedToggleProperty().addListener((s,a,b)-> {
tg.selectedToggleProperty().get();
bidir.get();
});
return bidir;
}
Upvotes: 0
Reputation: 1160
This answer is inspired by tunabot. Instead of using RadioButton
, this answer will use ToogleButton
, and to make it look more beautiful, we will use SegmentedButton
from ControlsFX. We can bind bidirectional selected toggle button by using valueProperty
from ToggleGroupValue
.
There is a debug button that when we click this button the selected button will change to DELETING
button.
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import jfxtras.scene.control.ToggleGroupValue;
import org.controlsfx.control.SegmentedButton;
public class ToggleBindingDemo extends Application{
public static void main(String[] args){
launch(args);
}
private final ObjectProperty<MouseMode> mouseModeObjectProperty = new SimpleObjectProperty<>(MouseMode.SELECTION);;
@Override
public void start(Stage stage){
ToggleGroupValue<MouseMode> toggleGroupValue = new ToggleGroupValue<>();
ToggleButton selection = new ToggleButton("Selection");
selection.setUserData(MouseMode.SELECTION);
selection.setToggleGroup(toggleGroupValue);
ToggleButton editing = new ToggleButton("Editing");
editing.setUserData(MouseMode.EDITING);
editing.setToggleGroup(toggleGroupValue);
ToggleButton deleting = new ToggleButton("Deleting");
deleting.setUserData(MouseMode.DELETING);
deleting.setToggleGroup(toggleGroupValue);
toggleGroupValue.valueProperty().bindBidirectional(mouseModeObjectProperty);
mouseModeObjectProperty.addListener(new ChangeListener<MouseMode>(){
@Override
public void changed(ObservableValue<? extends MouseMode> observable, MouseMode oldValue, MouseMode newValue){
System.out.println("MouseMode: " + newValue);
}
});
SegmentedButton segmentedButton = new SegmentedButton(selection, editing, deleting);
segmentedButton.setToggleGroup(toggleGroupValue);
Button debugButton = new Button("Debug");
debugButton.setOnMouseClicked(event -> handleDebugClick());
VBox vBox = new VBox(segmentedButton, debugButton);
vBox.setSpacing(10);
StackPane root = new StackPane(vBox);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
}
void handleDebugClick(){
mouseModeObjectProperty.set(MouseMode.DELETING);
}
public enum MouseMode{
SELECTION,
EDITING,
DELETING
}
}
Upvotes: 0
Reputation: 402
I have successfully made use of the ToggleGroupValue class in the JFXtras project.
Here is an example:
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class Main extends Application {
Child myChild = new Child();
@Override
public void start( Stage stage ) throws Exception {
stage.setTitle( "ToggleGroupValue Example" );
GridPane gridPane = new GridPane();
int rowIndex = 0;
gridPane.add( new Label("Nickname: "), 0, rowIndex );
ToggleGroupValue toggleGroupValue = new ToggleGroupValue();
rowIndex = createAddRadioButtons( gridPane, rowIndex, toggleGroupValue );
gridPane.add( new Label("Selected Nickname: "), 0, rowIndex );
Label selectedNickNameValueLabel = new Label();
gridPane.add( selectedNickNameValueLabel, 1, rowIndex );
myChild.nicknameProperty().bindBidirectional( toggleGroupValue.valueProperty() );
selectedNickNameValueLabel.textProperty().bind( toggleGroupValue.valueProperty() );
stage.setScene( new Scene( gridPane, 300, 100 ) );
stage.show();
}
private int createAddRadioButtons( GridPane gridPane, int rowIndex, ToggleGroupValue toggleGroupValue ) {
RadioButton radioButtonPunkin = new RadioButton();
radioButtonPunkin.setUserData( "Punkin" );
RadioButton radioButtonLittleBoy = new RadioButton();
radioButtonLittleBoy.setUserData( "Little Boy" );
RadioButton radioButtonBuddy = new RadioButton();
radioButtonBuddy.setUserData( "Buddy" );
List<RadioButton> radioButtons = Arrays.asList( radioButtonPunkin, radioButtonLittleBoy, radioButtonBuddy );
for ( RadioButton radioButton : radioButtons ) {
toggleGroupValue.add( radioButton, radioButton.getUserData() );
radioButton.setText( radioButton.getUserData().toString() );
gridPane.add( radioButton, 1, rowIndex++ );
}
return rowIndex;
}
private static class Child {
private StringProperty nickname = new SimpleStringProperty();
public StringProperty nicknameProperty() {
return nickname;
}
public String getNickname() {
return nickname.get();
}
public void setNickname( String notesProperty ) {
this.nickname.set( notesProperty );
}
}
public static void main(String[] args) {
launch(args);
}
}
Upvotes: 3
Reputation: 288
I'm using Java bean property adapter, but you can just use the last line of this code and bind it.
JavaBeanObjectProperty<fooEnum> property = null;
try {
property = new JavaBeanObjectPropertyBuilder<fooEnum>().bean(fooBean).name(fooField).build();
} catch (NoSuchMethodException e1) {
e1.printStackTrace();
}
property.addListener((obs, oldValue, newValue) -> {
System.out.println("Property value changed from " + oldValue + " to " + newValue);
});
BindingUtils.bindToggleGroupToProperty(fooToggleGroup, property);
You need to have a small BindingUtils class for ToggleGroup.
public final class BindingUtils {
private BindingUtils() {
}
public static <T> void bindToggleGroupToProperty(final ToggleGroup toggleGroup, final ObjectProperty<T> property) {
// Check all toggles for required user data
toggleGroup.getToggles().forEach(toggle -> {
if (toggle.getUserData() == null) {
throw new IllegalArgumentException("The ToggleGroup contains at least one Toggle without user data!");
}
});
// Select initial toggle for current property state
for (Toggle toggle : toggleGroup.getToggles()) {
if (property.getValue() != null && property.getValue().equals(toggle.getUserData())) {
toggleGroup.selectToggle(toggle);
break;
}
}
// Update property value on toggle selection changes
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
property.setValue((T) newValue.getUserData());
});
}
Upvotes: 1
Reputation: 209684
I don't think there is a way to do this directly. While a general-purpose
Bindings.bindBidirectional(Property<S> property1, Property<T> property2, Function<S,T> mapping, Function<T,S> inverseMapping)
might make a good addition to the API, even that wouldn't help in this case as the ToggleGroup
's selectedProperty
is read only (since selection needs to be handled when each Toggle
's setSelected(...)
method is invoked, as well as by the ToggleGroup
's selectedProperty
).
Using a couple of listeners is the way to go in this case.
The closest thing to the "custom bidirectional map" is the
Bindings.bindBiDirectional(StringProperty stringProperty, ObjectProperty<T> otherProperty, StringConverter<T> converter)
method. In the case where you have an (writeable) ObjectProperty<S>
and (writeable) ObjectProperty<T>
you can in theory use two bidirectional bindings and an intermediate StringProperty
to bind them together. In practice, this is almost always more code than just using two listeners, and is also less efficient.
Upvotes: 8