Commit 7eeea705 authored by Axel Howind's avatar Axel Howind

InputControl (in progress)

parent 2ae2f2a5
Pipeline #51207724 failed with stages
in 21 minutes and 6 seconds
......@@ -16,7 +16,6 @@ package com.dua3.fx.util.controls;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import javafx.scene.control.Dialog;
......
......@@ -3,9 +3,16 @@ package com.dua3.fx.util.controls;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
......@@ -46,20 +53,39 @@ public interface InputControl<R> {
default void init() {
// nop
}
/**
* Reset value to default
*/
void reset();
Property<R> valueProperty();
ReadOnlyBooleanProperty validProperty();
ReadOnlyStringProperty errorProperty();
class State<R> {
private final Property<R> value = new SimpleObjectProperty<>();
private final BooleanProperty valid = new SimpleBooleanProperty(true);
private final StringProperty error = new SimpleStringProperty("");
private Supplier<R> dflt;
private Function<R,Optional<String>> validate = s -> Optional.empty();
private static <R> Supplier<R> freeze(ObservableValue<R> value) {
final R frozen = value.getValue();
return () -> frozen;
}
public State(ObservableValue<R> value) {
this(value, freeze(value));
}
public State(ObservableValue<R> value, Supplier<R> dflt) {
this.value.bind(value);
this.value.addListener( (v,o,n) -> updateValidState(n) );
this.dflt = Objects.requireNonNull(dflt);
updateValidState(this.value.getValue());
}
......@@ -84,5 +110,13 @@ public interface InputControl<R> {
public Property<R> valueProperty() {
return value;
}
public void setDefault(Supplier<R> dflt) {
this.dflt = Objects.requireNonNull(dflt);
}
public void reset() {
value.setValue(dflt.get());
}
}
}
\ No newline at end of file
......@@ -22,7 +22,6 @@ import java.util.function.Supplier;
import com.dua3.utility.options.OptionSet;
import com.dua3.utility.options.OptionValues;
import javafx.scene.control.Label;
/**
* Builder for Alert Dialogs.
......
package com.dua3.fx.util.controls;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.logging.Logger;
import com.dua3.fx.util.FxUtil;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Dimension2D;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
public class InputPane extends InputDialogPane<Map<String,Object>> {
......@@ -91,11 +97,7 @@ public class InputPane extends InputDialogPane<Map<String,Object>> {
getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
final Button okButton = (Button) lookupButton(ButtonType.OK);
okButton.addEventFilter(ActionEvent.ACTION, ae -> {
if (!validateFields()) {
ae.consume(); //not valid
}
});
okButton.disableProperty().bind(Bindings.not(valid));
}
@Override
......@@ -138,33 +140,22 @@ public class InputPane extends InputDialogPane<Map<String,Object>> {
}
}
// FIXME Bindings.createBooleanBinding();
// valid state is true if all inputs are valid
ObservableBooleanValue[] inputs = validators.toArray(ObservableBooleanValue[]::new);
Callable<Boolean> check = () -> {
for (var value: inputs) {
if (!value.get()) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
};
BooleanBinding binding = Bindings.createBooleanBinding(check, inputs);
valid.bind(binding);
setContent(grid);
}
private boolean validateFields() {
// validateFields all input fields. validation succeeds if no validation returns an error message.
// do not use allMatches() because it might not process all items
return data.stream()
.map(this::validateAndMarkFields)
.filter(Optional::isPresent)
.count() == 0;
}
private Optional<String> validateAndMarkFields(Meta<?> item) {
Optional<String> result = item.validate();
boolean ok = result.isEmpty();
if (ok) {
item.marker.setText(MARKER_OK);
item.marker.setTooltip(null);
} else {
item.marker.setText(MARKER_ERROR);
item.marker.setTooltip(new Tooltip(result.get()));
}
return result;
}
protected BooleanProperty valid = new SimpleBooleanProperty(false);
public ReadOnlyBooleanProperty validProperty() {
......
......@@ -14,13 +14,13 @@
package com.dua3.fx.util.controls;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import com.dua3.fx.util.controls.InputPane.Meta;
......@@ -28,9 +28,14 @@ import com.dua3.utility.lang.LangUtil;
import com.dua3.utility.options.OptionSet;
import com.dua3.utility.options.OptionValues;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.Node;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableStringValue;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
......@@ -87,54 +92,16 @@ implements InputBuilder<InputPaneBuilder> {
return this;
}
static class AbstractInputControl<R> implements InputControl<R> {
private final BooleanProperty valid = new SimpleBooleanProperty(true);
private final StringProperty error = new SimpleStringProperty("");
private Function<R,Optional<String>> validate = s -> Optional.empty();
public void setValidate(Function<R,Optional<String>> validate) {
this.validate = Objects.requireNonNull(validate);
}
protected void updateValidState(R r) {
Optional<String> result = validate.apply(r);
valid.setValue(result.isEmpty());
error.setValue(result.orElse(""));
}
@Override
public ReadOnlyBooleanProperty validProperty() {
return valid;
}
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#text(java.lang.String, java.lang.String, java.lang.String, java.util.function.Function)
*/
@Override
public InputPaneBuilder string(String id, String label, Supplier<String> dflt, Function<String,Optional<String>> validate) {
TextField control = new TextField();
ObservableStringValue value = control.textProperty();
return add(id, label, String.class, dflt,
new InputControl<>() {
final TextField control = new TextField();
@Override
public Node node() {
return control;
}
@Override
public String get() {
return control.getText();
}
@Override
public void set(String arg) {
control.setText(arg);
}
});
new SimpleInputControl<>(control, value, dflt)
);
}
/* (non-Javadoc)
......@@ -142,36 +109,14 @@ implements InputBuilder<InputPaneBuilder> {
*/
@Override
public InputPaneBuilder integer(String id, String label, Supplier<Integer> dflt, Function<Integer,Optional<String>> validate) {
return add(id, label, Integer.class, dflt,
new InputControl<>() {
final TextField control = new TextField();
@Override
public Node node() {
return control;
}
@Override
public Integer get() {
return Integer.parseInt(control.getText());
}
TextField control = new TextField();
StringProperty textProperty = control.textProperty();
IntegerProperty value = new SimpleIntegerProperty();
textProperty.bindBidirectional(value, NumberFormat.getIntegerInstance());
@Override
public void set(Integer arg) {
control.setText(Integer.toString(arg));
}
@Override
public Optional<String> validate() {
String t = control.getText();
try {
int i = Integer.parseInt(t);
return validate.apply(i);
} catch (@SuppressWarnings("unused") NumberFormatException e) {
return Optional.of("'"+t+"' is no valid integer.");
}
}
});
return add(id, label, Integer.class, dflt,
new SimpleInputControl<>(control, value.asObject(), dflt)
);
}
/* (non-Javadoc)
......@@ -179,36 +124,14 @@ implements InputBuilder<InputPaneBuilder> {
*/
@Override
public InputPaneBuilder decimal(String id, String label, Supplier<Double> dflt, Function<Double,Optional<String>> validate) {
return add(id, label, Double.class, dflt,
new InputControl<>() {
final TextField control = new TextField();
@Override
public Node node() {
return control;
}
@Override
public Double get() {
return Double.parseDouble(control.getText());
}
TextField control = new TextField();
StringProperty textProperty = control.textProperty();
DoubleProperty value = new SimpleDoubleProperty();
textProperty.bindBidirectional(value, NumberFormat.getInstance());
@Override
public void set(Double arg) {
control.setText(Double.toString(arg));
}
@Override
public Optional<String> validate() {
String t = control.getText();
try {
double d = Double.parseDouble(t);
return validate.apply(d);
} catch (@SuppressWarnings("unused") NumberFormatException e) {
return Optional.of("'"+t+"' is no valid number.");
}
}
});
return add(id, label, Double.class, dflt,
new SimpleInputControl<>(control, value.asObject(), dflt)
);
}
/* (non-Javadoc)
......@@ -216,34 +139,11 @@ implements InputBuilder<InputPaneBuilder> {
*/
@Override
public InputPaneBuilder checkBox(String id, String label, Supplier<Boolean> dflt, String text) {
CheckBox control = new CheckBox(text);
BooleanProperty value = control.selectedProperty();
return add(id, label, Boolean.class, dflt,
new InputControl<>() {
final CheckBox control = new CheckBox(text);
{
control.setSelected(dflt.get());
}
@Override
public Node node() {
return control;
}
@Override
public Boolean get() {
return control.isSelected();
}
@Override
public void set(Boolean arg) {
control.setSelected(arg);
}
@Override
public Optional<String> validate() {
return Optional.empty();
}
});
new SimpleInputControl<>(control, value, dflt)
);
}
/* (non-Javadoc)
......@@ -251,39 +151,11 @@ implements InputBuilder<InputPaneBuilder> {
*/
@Override
public <T> InputPaneBuilder comboBox(String id, String label, Supplier<T> dflt, Class<T> cls, Collection<T> items) {
ComboBox<T> control = new ComboBox<T>();
ObjectProperty<T> value = control.valueProperty();
return add(id, label, cls, dflt,
new InputControl<>() {
final ComboBox<T> control = new ComboBox<>();
{
control.setItems(FXCollections.observableArrayList(items));
control.setValue(dflt.get());
}
@Override
public Node node() {
return control;
}
@Override
public T get() {
return control.getValue();
}
@Override
public void set(T arg) {
control.setValue(arg);
}
@Override
public Optional<String> validate() {
T t = control.getValue();
if (t == null) {
return Optional.of("No value selected.");
}
return Optional.empty();
}
});
new SimpleInputControl<>(control, value, dflt)
);
}
/* (non-Javadoc)
......
......@@ -14,6 +14,7 @@ import com.dua3.utility.options.OptionValues;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Node;
......@@ -28,10 +29,14 @@ public class OptionsPane extends GridPane implements InputControl<OptionValues>{
/** Logger */
protected static final Logger LOG = Logger.getLogger(OptionsPane.class.getSimpleName());
private final InputControl.State<OptionValues> state;
private Supplier<OptionSet> options;
private Supplier<OptionValues> currentValues;
private Supplier<OptionValues> dflt;
private ObservableValue<OptionValues> value = new SimpleObjectProperty<>();
private LinkedHashMap<Option<?>, Property<?>> items = new LinkedHashMap<>();
private static final Insets INSETS = new Insets(2);
......@@ -48,20 +53,21 @@ public class OptionsPane extends GridPane implements InputControl<OptionValues>{
public OptionsPane(Supplier<OptionSet> options, Supplier<OptionValues> dflt) {
this.options = options;
this.currentValues=dflt;
this.dflt=dflt;
this.state = new State<>(value, dflt);
}
public void init() {
getChildren().clear();
OptionSet optionSet = options.get();
OptionValues values = currentValues.get();
OptionValues values = state.valueProperty().getValue();
int row = 0;
for (Option<?> option: optionSet) {
Label label = new Label(option.getName());
Property<?> property;
Property<? extends Value<?>> valueProperty;
Control control;
Value<?> value = values.get(option);
if (option instanceof StringOption) {
......@@ -69,31 +75,34 @@ public class OptionsPane extends GridPane implements InputControl<OptionValues>{
c.setText(value.text());
control = c;
StringProperty textProperty = c.textProperty();
Property<Value<String>> valueProperty = new SimpleObjectProperty<Value<String>>();
Property<Value<String>> property = new SimpleObjectProperty<Value<String>>();
textProperty.addListener((v,o,n) -> {
valueProperty.setValue(Option.value(n));
property.setValue(Option.value(n));
});
valueProperty.addListener((v,o,n) -> textProperty.set(n.get()));
property = valueProperty;
property.addListener((v,o,n) -> textProperty.set(n.get()));
valueProperty = property;
} else if (option instanceof ChoiceOption<?>) {
var choices = FXCollections.observableList(((ChoiceOption<?>)option).getChoices());
var c = new ComboBox<>(choices);
c.getSelectionModel().select(choices.indexOf(value));
control = c;
property = c.valueProperty();
valueProperty = c.valueProperty();
} else {
LOG.warning("unknown option type: "+option.getClass().getName());
control = null;
property = null;
valueProperty = null;
}
items.put(option, property);
items.put(option, valueProperty);
addToGrid(label, 0, row);
addToGrid(control, 1, row);
row++;
}
// create binding
}
private void addToGrid(Control child, int c, int r) {
......
......@@ -2,13 +2,12 @@ package com.dua3.fx.util.controls;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.logging.Logger;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.*;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Node;
......@@ -27,12 +26,12 @@ public class RadioPane<T> extends VBox implements InputControl<T> {
private static final double SPACING = 4;
private final InputControl.State state;
private final InputControl.State<T> state;
/**
* Create new Radio Pane.
* @param items
* the selectable items
* the available items
* @param currentValue
* the current value
*/
......@@ -49,13 +48,21 @@ public class RadioPane<T> extends VBox implements InputControl<T> {
this.items.put(item, control);
}
group.selectToggle(this.items.get(currentValue));
// update state when selected toggle changes
@SuppressWarnings("unchecked")
ObservableValue<T> valueBinding = Bindings.createObjectBinding( () -> {
Toggle selectedToggle = group.getSelectedToggle();
return selectedToggle != null ? (T) selectedToggle.getUserData() : null;
}, group.selectedToggleProperty());
this.state = new State(valueBinding);
this.state = new State<>(valueBinding);
// update toggle, when state changes
state.valueProperty().addListener( (v,o,n) -> {
group.selectToggle(this.items.get(n));
});
// set initial toggle
group.selectToggle(this.items.get(currentValue));
}
@Override
......@@ -77,4 +84,9 @@ public class RadioPane<T> extends VBox implements InputControl<T> {
public ReadOnlyStringProperty errorProperty() {
return state.errorProperty();
}
@Override
public void reset() {
state.reset();
}
}
package com.dua3.fx.util.controls;
import java.util.Objects;
import java.util.function.Supplier;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Control;
class SimpleInputControl<C extends Control,R> implements InputControl<R> {
private final C control;
private final State<R> state;
private final Supplier<R> dflt;
protected SimpleInputControl(C control, ObservableValue<R> value, Supplier<R> dflt) {
this.control = Objects.requireNonNull(control);
this.state = new State<>(value);
this.dflt = dflt;
reset();
}
public void reset() {
state.valueProperty().setValue(dflt.get());
}
@Override
public C node() {
return control;
}
@Override
public Property<R> valueProperty() {
return state.valueProperty();
}
@Override
public ReadOnlyBooleanProperty validProperty() {
return state.validProperty();
}
@Override
public ReadOnlyStringProperty errorProperty() {
return state.errorProperty();
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment