Commit 6fe9c34f authored by Axel Howind's avatar Axel Howind

extract InputGrid

parent dc60a61e
Pipeline #55337031 failed with stages
in 16 minutes and 27 seconds
......@@ -14,6 +14,21 @@
package com.dua3.fx.application;
import com.dua3.fx.util.Dialogs;
import com.dua3.fx.util.FxTask;
import com.dua3.fx.util.controls.AboutDialog;
import com.dua3.utility.lang.LangUtil;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ButtonType;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
......@@ -31,22 +46,6 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import com.dua3.fx.util.Dialogs;
import com.dua3.fx.util.FxTask;
import com.dua3.fx.util.controls.AboutDialog;
import com.dua3.utility.lang.LangUtil;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ButtonType;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
public abstract class FxController<A extends FxApplication<A, C>, C extends FxController<A, C>> {
// - static -
......@@ -134,9 +133,10 @@ public abstract class FxController<A extends FxApplication<A, C>, C extends FxCo
AtomicBoolean goOn = new AtomicBoolean(false);
Dialogs.confirmation()
.header("Save changes?")
.text(String.join(
"\n",
dirtyList.stream().map(Object::toString).toArray(String[]::new)
.text("%s",
String.join(
"\n",
dirtyList.stream().map(Object::toString).toArray(String[]::new)
))
.buttons(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL)
.defaultButton(ButtonType.YES)
......
......@@ -150,14 +150,23 @@ public class Dialogs {
}
/**
* Start definition of new input dialog.
* @return
* new {@link InputDialogBuilder} instance
* Start definition of new input pane.
* @return
* new {@link InputPaneBuilder} instance
*/
public static InputPaneBuilder inputPane() {
return new InputPaneBuilder();
}
/**
* Start definition of new input dialog.
* @return
* new {@link InputDialogBuilder} instance
*/
public static InputGridBuilder inputGrid() {
return new InputGridBuilder();
}
/**
* Start definition of new options dialog.
* @return
......
package com.dua3.fx.util.controls;
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.beans.value.ObservableBooleanValue;
import javafx.geometry.Dimension2D;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.logging.Logger;
public class InputGrid extends GridPane {
/** Logger */
protected static final Logger LOG = Logger.getLogger(InputGrid.class.getSimpleName());
private static final String MARKER_INITIAL = "";
private static final String MARKER_ERROR = "\u26A0";
private static final String MARKER_OK = "";
protected final BooleanProperty valid = new SimpleBooleanProperty(true);
/**
* Get valid state property.
* @return the valid state property of the input
*/
public ReadOnlyBooleanProperty validProperty() {
return valid;
}
/**
* Meta data for a single input field consisting of ID, label text, default value etc.
*
* @param <T>
* the input's value type
*/
static class Meta<T> {
final String id;
final Class<T> cls;
final Supplier<T> dflt;
final InputControl<T> control;
Label label = new Label();
Label marker = new Label();
Meta(String id, String label, Class<T> cls, Supplier<T> dflt, InputControl<T> control) {
this.id = id;
this.label.setText(label);
this.cls = cls;
this.dflt = dflt;
this.control = control;
Dimension2D dimMarker = new Dimension2D(0,0);
dimMarker = FxUtil.growToFit(dimMarker, marker.getBoundsInLocal());
marker.setMinSize(dimMarker.getWidth(), dimMarker.getHeight());
this.marker.setText(MARKER_INITIAL);
}
void reset() {
control.set(dflt.get());
}
}
private Collection<Meta<?>> data = null;
private int columns = 1;
public Map<String, Object> get() {
// Collecors.toMap() does not support null values!
Map<String,Object> result = new HashMap<>();
data.forEach(e -> result.put(e.id, e.control.get()));
return result;
}
public InputGrid() {
}
private void addToGrid(Node child, int c, int r, int span, Insets insets) {
add(child, c, r, span, 1);
GridPane.setMargin(child, insets);
}
void setContent(Collection<Meta<?>> data, int columns) {
this.data = Objects.requireNonNull(data);
this.columns = columns;
}
public void init() {
getChildren().clear();
List<BooleanExpression> validators = new ArrayList<>();
// create grid with input controls
Insets insets = new Insets(2);
Insets markerInsets = new Insets(0);
int r = 0, c = 0;
for (var entry : data) {
// add label and control
int gridX = 3*c;
int gridY = r;
int span;
if (entry.label != null) {
addToGrid(entry.label, gridX, gridY, 1, insets);
gridX++;
span = 1;
} else {
span = 2;
}
validators.add(entry.control.validProperty());
addToGrid(entry.control.node(), gridX, gridY, span, insets);
gridX += span;
addToGrid(entry.marker, gridX, gridY, 1, markerInsets);
entry.control.init();
// move to next position in grid
c = (c + 1) % columns;
if (c == 0) {
r++;
}
}
// 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);
for (var entry: data) {
entry.control.node().requestFocus();
break;
}
}
public void reset() {
data.forEach(entry -> entry.control.reset());
}
}
// Copyright 2019 Axel Howind
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.dua3.fx.util.controls;
import com.dua3.fx.util.controls.InputGrid.Meta;
import com.dua3.utility.lang.LangUtil;
import com.dua3.utility.options.OptionSet;
import com.dua3.utility.options.OptionValues;
import javafx.stage.FileChooser;
import java.io.File;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Builder for Alert Dialogs.
*
* Provides a fluent interface to create Alerts.
*/
public class InputGridBuilder
implements InputBuilder<InputGridBuilder> {
public InputGridBuilder() {
super();
}
private int columns = 1;
private LinkedHashMap<String, InputGrid.Meta<?>> data = new LinkedHashMap<>();
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#add(java.lang.String, java.lang.String, java.lang.Class, T, com.dua3.fx.util.controls.InputDialogPane.InputControl)
*/
@Override
public <T> InputGridBuilder add(String id, String label, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
Objects.requireNonNull(id);
Meta<T> meta = new Meta<>(id, label, type, dflt, control);
Meta<?> prev = data.put(id, meta);
LangUtil.check(prev == null, "Input with id '" + id + "' already defined");
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#add(java.lang.String, java.lang.String, java.lang.Class, T, com.dua3.fx.util.controls.InputDialogPane.InputControl)
*/
@Override
public <T> InputGridBuilder add(String id, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
Objects.requireNonNull(id);
Meta<T> meta = new Meta<>(id, null, type, dflt, control);
Meta<?> prev = data.put(id, meta);
LangUtil.check(prev == null, "Input with id '" + id + "' already defined");
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#columns(int)
*/
@Override
public InputGridBuilder columns(int columns) {
LangUtil.check(columns > 0);
this.columns = columns;
return this;
}
/* (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 InputGridBuilder string(String id, String label, Supplier<String> dflt, Function<String,Optional<String>> validate) {
return add(id, label, String.class, dflt, InputControl.stringInput(dflt, validate));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#integer(java.lang.String, java.lang.String, java.lang.Integer, java.util.function.Function)
*/
@Override
public InputGridBuilder integer(String id, String label, Supplier<Integer> dflt, Function<Integer,Optional<String>> validate) {
return add(id, label, Integer.class, dflt, InputControl.integerInput(dflt, validate));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#decimal(java.lang.String, java.lang.String, java.lang.Double, java.util.function.Function)
*/
@Override
public InputGridBuilder decimal(String id, String label, Supplier<Double> dflt, Function<Double,Optional<String>> validate) {
return add(id, label, Double.class, dflt, InputControl.decimalInput(dflt, validate));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#checkBox(java.lang.String, java.lang.String, boolean, java.lang.String)
*/
@Override
public InputGridBuilder checkBox(String id, String label, Supplier<Boolean> dflt, String text) {
return add(id, label, Boolean.class, dflt, InputControl.checkBoxInput(dflt, text));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#comboBox(java.lang.String, java.lang.String, T, java.lang.Class, java.util.Collection)
*/
@Override
public <T> InputGridBuilder comboBox(String id, String label, Supplier<T> dflt, Class<T> cls, Collection<T> items) {
return add(id, label, cls, dflt, InputControl.comboBoxInput(items, dflt));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#radioList(java.lang.String, java.lang.String, T, java.lang.Class, java.util.Collection)
*/
@Override
public <T> InputGridBuilder radioList(String id, String label, Supplier<T> dflt, Class<T> cls, Collection<T> items) {
return add(id, label, cls, dflt, new RadioPane<>(items, null));
}
@Override
public InputGridBuilder options(String id, String label, Supplier<OptionValues> dflt, Supplier<OptionSet> options) {
return add(id, label, OptionValues.class, dflt, new OptionsPane(options, dflt));
}
public InputGridBuilder options(String id, Supplier<OptionValues> dflt, Supplier<OptionSet> options) {
return add(id, OptionValues.class, dflt, new OptionsPane(options, dflt));
}
@Override
public InputGridBuilder chooseFile(String id, String label, Supplier<File> dflt, FileDialogMode mode, FileChooser.ExtensionFilter filter) {
return add(id, label, File.class, dflt, new FileInput(mode, dflt, filter));
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#build()
*/
public InputGrid build() {
InputGrid grid = new InputGrid();
grid.setContent(data.values(), columns);
return grid;
}
}
package com.dua3.fx.util.controls;
import com.dua3.fx.util.FxUtil;
import com.dua3.utility.data.Pair;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Dimension2D;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Logger;
public class InputPane extends InputDialogPane<Map<String,Object>> {
......@@ -24,128 +11,25 @@ public class InputPane extends InputDialogPane<Map<String,Object>> {
/** Logger */
protected static final Logger LOG = Logger.getLogger(InputPane.class.getSimpleName());
private static final String MARKER_INITIAL = "";
private static final String MARKER_ERROR = "\u26A0";
private static final String MARKER_OK = "";
private GridPane grid = new GridPane();
/**
* Meta data for a single input field consisting of ID, label text, default value etc.
*
* @param <T>
* the input's value type
*/
static class Meta<T> {
final String id;
final Class<T> cls;
final Supplier<T> dflt;
final InputControl<T> control;
Label label = new Label();
Label marker = new Label();
Meta(String id, String label, Class<T> cls, Supplier<T> dflt, InputControl<T> control) {
this.id = id;
this.label.setText(label);
this.cls = cls;
this.dflt = dflt;
this.control = control;
Dimension2D dimMarker = new Dimension2D(0,0);
dimMarker = FxUtil.growToFit(dimMarker, marker.getBoundsInLocal());
marker.setMinSize(dimMarker.getWidth(), dimMarker.getHeight());
this.marker.setText(MARKER_INITIAL);
}
void reset() {
control.set(dflt.get());
}
}
private Collection<Meta<?>> data = null;
private int columns = 1;
private final InputGrid inputGrid;
@Override
public Map<String, Object> get() {
// Collecors.toMap() does not support null values!
Map<String,Object> result = new HashMap<>();
data.forEach(e -> result.put(e.id, e.control.get()));
return result;
}
public InputPane() {
}
private void addToGrid(Node child, int c, int r, int span, Insets insets) {
grid.add(child, c, r, span, 1);
GridPane.setMargin(child, insets);
Node content = getContent();
if (content instanceof InputGrid) {
return ((InputGrid) content).get();
} else {
return Collections.emptyMap();
}
}
void setContent(Collection<Meta<?>> data, int columns, List<Pair<ButtonType, Consumer<InputDialogPane>>> buttons) {
this.data = Objects.requireNonNull(data);
this.columns = columns;
this.buttons = new ArrayList<>(buttons);
public InputPane(InputGrid inputGrid) {
this.inputGrid = inputGrid;
setContent(inputGrid);
}
@Override
public void init() {
grid.getChildren().clear();
List<BooleanExpression> validators = new ArrayList<>();
// create grid with input controls
Insets insets = new Insets(2);
Insets markerInsets = new Insets(0);
int r = 0, c = 0;
for (var entry : data) {
// add label and control
int gridX = 3*c;
int gridY = r;
int span;
if (entry.label != null) {
addToGrid(entry.label, gridX, gridY, 1, insets);
gridX++;
span = 1;
} else {
span = 2;
}
validators.add(entry.control.validProperty());
addToGrid(entry.control.node(), gridX, gridY, span, insets);
gridX += span;
addToGrid(entry.marker, gridX, gridY, 1, markerInsets);
entry.control.init();
// move to next position in grid
c = (c + 1) % columns;
if (c == 0) {
r++;
}
}
// 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);
for (var entry: data) {
entry.control.node().requestFocus();
break;
}
inputGrid.init();
}
}
......@@ -14,9 +14,7 @@
package com.dua3.fx.util.controls;
import com.dua3.fx.util.controls.InputPane.Meta;
import com.dua3.utility.data.Pair;
import com.dua3.utility.lang.LangUtil;
import com.dua3.utility.options.OptionSet;
import com.dua3.utility.options.OptionValues;
import javafx.scene.control.ButtonType;
......@@ -30,7 +28,7 @@ import java.util.function.Supplier;
/**
* Builder for Alert Dialogs.
*
*
* Provides a fluent interface to create Alerts.
*/
public class InputPaneBuilder
......@@ -39,112 +37,85 @@ implements InputBuilder<InputPaneBuilder> {
public InputPaneBuilder() {
super();
setDialogSupplier(InputPane::new);
setDialogSupplier(() -> new InputPane(pb.build()));
}
private int columns = 1;
private final InputGridBuilder pb = new InputGridBuilder();
private LinkedHashMap<String, InputPane.Meta<?>> data = new LinkedHashMap<>();
private List<Pair<ButtonType,Consumer<InputDialogPane>>> buttons = new LinkedList<>();
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#add(java.lang.String, java.lang.String, java.lang.Class, T, com.dua3.fx.util.controls.InputDialogPane.InputControl)
*/
@Override
public <T> InputPaneBuilder add(String id, String label, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
Objects.requireNonNull(id);
Meta<T> meta = new Meta<>(id, label, type, dflt, control);
Meta<?> prev = data.put(id, meta);
LangUtil.check(prev == null, "Input with id '" + id + "' already defined");
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#add(java.lang.String, java.lang.String, java.lang.Class, T, com.dua3.fx.util.controls.InputDialogPane.InputControl)
*/
@Override
public <T> InputPaneBuilder add(String id, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
Objects.requireNonNull(id);
Meta<T> meta = new Meta<>(id, null, type, dflt, control);
Meta<?> prev = data.put(id, meta);
LangUtil.check(prev == null, "Input with id '" + id + "' already defined");
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#columns(int)
*/
@Override
public InputPaneBuilder columns(int columns) {
LangUtil.check(columns > 0);
this.columns = columns;
public <T> InputPaneBuilder add(String id, String label, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
pb.add(id, label, type, dflt, control);
return this;
}
/* (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) {
return add(id, label, String.class, dflt, InputControl.stringInput(dflt, validate));
public <T> InputPaneBuilder add(String id, Class<T> type, Supplier<T> dflt, InputControl<T> control) {
pb.add(id, type, dflt, control);
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#integer(java.lang.String, java.lang.String, java.lang.Integer, java.util.function.Function)
*/
@Override
public InputPaneBuilder integer(String id, String label, Supplier<Integer> dflt, Function<Integer,Optional<String>> validate) {
return add(id, label, Integer.class, dflt, InputControl.integerInput(dflt, validate));
public InputPaneBuilder columns(int columns) {
pb.columns(columns);
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#decimal(java.lang.String, java.lang.String, java.lang.Double, java.util.function.Function)
*/
@Override
public InputPaneBuilder decimal(String id, String label, Supplier<Double> dflt, Function<Double,Optional<String>> validate) {
return add(id, label, Double.class, dflt, InputControl.decimalInput(dflt, validate));
public InputPaneBuilder string(String id, String label, Supplier<String> dflt, Function<String,Optional<String>> validate) {
pb.string(id, label, dflt, validate);
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#checkBox(java.lang.String, java.lang.String, boolean, java.lang.String)
*/
@Override
public InputPaneBuilder checkBox(String id, String label, Supplier<Boolean> dflt, String text) {
return add(id, label, Boolean.class, dflt, InputControl.checkBoxInput(dflt, text));
public InputPaneBuilder integer(String id, String label, Supplier<Integer> dflt, Function<Integer,Optional<String>> validate) {
pb.integer(id, label, dflt, validate);
return this;
}
/* (non-Javadoc)
* @see com.dua3.fx.util.controls.InputBuilder#comboBox(java.lang.String, java.lang.String, T, java.lang.Class, java.util.Collection)
*/
@Override
public <T> InputPaneBuilder comboBox(String id, String label, Supplier<T> dflt, Class<T> cls, Collection<T> items) {<