net.officefloor.eclipse.configurer.internal.AbstractConfigurationBuilder Maven / Gradle / Ivy
/*
* OfficeFloor - http://www.officefloor.net
* Copyright (C) 2005-2018 Daniel Sagenschneider
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package net.officefloor.eclipse.configurer.internal;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import org.eclipse.swt.widgets.Shell;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import net.officefloor.eclipse.configurer.Actioner;
import net.officefloor.eclipse.configurer.Builder;
import net.officefloor.eclipse.configurer.ChoiceBuilder;
import net.officefloor.eclipse.configurer.ClassBuilder;
import net.officefloor.eclipse.configurer.CloseListener;
import net.officefloor.eclipse.configurer.Configuration;
import net.officefloor.eclipse.configurer.ConfigurationBuilder;
import net.officefloor.eclipse.configurer.DefaultImages;
import net.officefloor.eclipse.configurer.ErrorListener;
import net.officefloor.eclipse.configurer.FlagBuilder;
import net.officefloor.eclipse.configurer.ListBuilder;
import net.officefloor.eclipse.configurer.MappingBuilder;
import net.officefloor.eclipse.configurer.MultipleBuilder;
import net.officefloor.eclipse.configurer.PropertiesBuilder;
import net.officefloor.eclipse.configurer.ResourceBuilder;
import net.officefloor.eclipse.configurer.TextBuilder;
import net.officefloor.eclipse.configurer.ValueValidator;
import net.officefloor.eclipse.configurer.ValueValidator.ValueValidatorContext;
import net.officefloor.eclipse.configurer.internal.inputs.ChoiceBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.ClassBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.FlagBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.ListBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.MappingBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.MultipleBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.PropertiesBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.ResourceBuilderImpl;
import net.officefloor.eclipse.configurer.internal.inputs.TextBuilderImpl;
import net.officefloor.eclipse.osgi.OfficeFloorOsgiBridge;
/**
* Abstract {@link ConfigurationBuilder}.
*
* @author Daniel Sagenschneider
*/
public abstract class AbstractConfigurationBuilder implements ConfigurationBuilder {
/**
* CSS class applied to {@link GridPane} in wide view.
*/
public static final String CSS_CLASS_WIDE = "wide";
/**
* CSS class applied to the {@link GridPane} in narrow view.
*/
public static final String CSS_CLASS_NARROW = "narrow";
/**
* Listing of the {@link ValueRendererFactory} instances.
*/
private final List> rendererFactories = new ArrayList<>();
/**
* {@link OfficeFloorOsgiBridge}.
*/
private final OfficeFloorOsgiBridge osgiBridge;
/**
* Parent {@link Shell} for dialogs.
*/
private final Shell parentShell;
/**
* Title.
*/
private String title = null;
/**
* {@link ValueValidator}.
*/
private ValueValidator validator = null;
/**
* {@link ErrorListener}.
*/
private ErrorListener errorListener = null;
/**
* Label for the apply {@link Actioner}.
*/
private String applierLabel = null;
/**
* {@link Applier} to apply the model.
*/
private Applier applier = null;
/**
* {@link CloseListener}.
*/
private CloseListener closeListener = null;
/**
* Instantiate.
*
* @param osgiBridge
* {@link OfficeFloorOsgiBridge}.
* @param parentShell
* Parent {@link Shell}. May be null if no dialog
* configuration required by inputs.
*/
public AbstractConfigurationBuilder(OfficeFloorOsgiBridge osgiBridge, Shell parentShell) {
this.osgiBridge = osgiBridge;
this.parentShell = parentShell;
}
/**
* Obtains the parent {@link Shell}.
*
* @return Parent {@link Shell}.
*/
public Shell getParentShell() {
if (this.parentShell == null) {
throw new IllegalStateException("Not initialised against a parent " + Shell.class.getSimpleName()
+ " but requiring dialog based configuration");
}
return this.parentShell;
}
/**
* Obtain the list of {@link ValueRendererFactory} instances.
*
* @return {@link ValueRendererFactory} instances.
*/
@SuppressWarnings("unchecked")
public ValueRendererFactory[] getValueRendererFactories() {
return this.rendererFactories.toArray(new ValueRendererFactory[this.rendererFactories.size()]);
}
/**
* Loads the configuration to the parent {@link Pane}.
*
* @param model
* Model.
* @param configurationNode
* Configuration {@link Pane}.
* @return {@link Configuration}.
*/
protected Configuration loadConfiguration(M model, Pane configurationNode) {
// Load the default styling
Scene scene = configurationNode.getScene();
if (scene != null) {
scene.getStylesheets().add(
AbstractConfigurationBuilder.class.getPackage().getName().replace('.', '/') + "/Configurer.css");
}
// Create the dirty and valid properties
Property dirtyProperty = new SimpleBooleanProperty(false);
SimpleBooleanProperty validProperty = new SimpleBooleanProperty(true);
// Create the actioner
ActionerImpl actioner = null;
if (this.applier != null) {
actioner = new ActionerImpl<>(model, this.applierLabel, this.applier, dirtyProperty, this.closeListener);
}
// Scroll both narrow and wide views
ScrollPane scroll = new ScrollPane() {
@Override
public void requestFocus() {
// avoid stealing focus
// (work around for GEF drag/drop aborting on focus change)
}
};
scroll.prefWidthProperty().bind(configurationNode.widthProperty());
scroll.prefHeightProperty().bind(configurationNode.heightProperty());
scroll.setFitToWidth(true);
scroll.getStyleClass().add("configurer-container");
// Provide grid to load configuration
GridPane grid = new GridPane();
scroll.setContent(grid);
// Determine if provided an error listener
ErrorListener errorListener = this.errorListener;
DefaultErrorListener defaultErrorListener = null;
if (errorListener == null) {
// Provide wrappers for action and error listening
VBox wrapper = new VBox();
wrapper.getStyleClass().setAll("configurer", "dialog-pane");
wrapper.pseudoClassStateChanged(PseudoClass.getPseudoClass("header"), true);
configurationNode.getChildren().add(wrapper);
// Create the error listener
defaultErrorListener = new DefaultErrorListener<>(this.title, actioner, wrapper, scroll, this.closeListener,
dirtyProperty, validProperty);
errorListener = defaultErrorListener;
// Bind height of scroll (minus header)
scroll.prefHeightProperty().bind(Bindings.subtract(configurationNode.heightProperty(),
defaultErrorListener.header.heightProperty()));
// Load header and configuration
wrapper.getChildren().setAll(defaultErrorListener.header, scroll);
} else {
// Just configuration node (as error listening provided)
scroll.getStyleClass().setAll("configurer");
scroll.pseudoClassStateChanged(PseudoClass.getPseudoClass("no-header"), true);
configurationNode.getChildren().add(scroll);
}
// Allow action to notify of apply error
if (actioner != null) {
actioner.errorListener = errorListener;
}
// Apply CSS (so Scene available to inputs)
configurationNode.applyCss();
// Load the configuration to grid
Configuration configuration = this.recursiveLoadConfiguration(model, configurationNode, grid, actioner,
dirtyProperty, validProperty, errorListener);
// Setup to display heading (with apply button disabled)
if (defaultErrorListener != null) {
defaultErrorListener.valid();
}
// Return the configuration
return configuration;
}
/**
* Loads the configuration to the {@link GridPane}.
*
* @param model
* Model.
* @param configurationNode
* Configuration {@link Node}.
* @param grid
* {@link GridPane}.
* @param actioner
* {@link Actioner}.
* @param dirtyProperty
* Dirty {@link Property}.
* @param validProperty
* Valid {@link Property}.
* @param errorListener
* {@link ErrorListener}.
* @return {@link Configuration}.
*/
public Configuration recursiveLoadConfiguration(M model, Node configurationNode, GridPane grid, Actioner actioner,
Property dirtyProperty, Property validProperty, ErrorListener errorListener) {
// Apply CSS (so Scene available to inputs)
grid.applyCss();
// Load the value render list
ValueLister lister = new ValueLister<>(model, this.validator, configurationNode, grid, this.title,
dirtyProperty, validProperty, errorListener, actioner, this.getValueRendererFactories(), null);
lister.organiseWide(1); // ensure initially organised
// Responsive view
final double RESPONSIVE_WIDTH = 800;
InvalidationListener listener = (event) -> {
if (grid.getWidth() < RESPONSIVE_WIDTH) {
// Avoid events if already narrow
if (lister.isWideNotNarrow) {
lister.organiseNarrow(1);
}
} else {
// Again avoid events if already wide
if (!lister.isWideNotNarrow) {
lister.organiseWide(1);
}
}
};
grid.widthProperty().addListener(listener);
listener.invalidated(null); // organise initial view
// Ensure display potential error
lister.refreshError();
// Return the configuration
return lister;
}
/**
* Convenience method to register the {@link ValueRendererFactory}.
*
* @param builder
* Builder.
* @return Input builder.
*/
private > B registerBuilder(B builder) {
this.rendererFactories.add(builder);
return builder;
}
/*
* ================== ConfigurationBuilder ====================
*/
@Override
public ConfigurationBuilder title(String title) {
this.title = title;
return this;
}
@Override
public TextBuilder text(String label) {
return this.registerBuilder(new TextBuilderImpl<>(label));
}
@Override
public FlagBuilder flag(String label) {
return this.registerBuilder(new FlagBuilderImpl<>(label));
}
@Override
public ChoiceBuilder choices(String label) {
return this.registerBuilder(new ChoiceBuilderImpl<>(label, this.osgiBridge, this.parentShell));
}
@Override
public ListBuilder list(String label, Class itemType) {
return this.registerBuilder(new ListBuilderImpl<>(label));
}
@Override
public MultipleBuilder multiple(String label, Class itemType) {
return this.registerBuilder(new MultipleBuilderImpl<>(label, this.osgiBridge, this.parentShell));
}
@Override
public PropertiesBuilder properties(String label) {
return this.registerBuilder(new PropertiesBuilderImpl<>(label));
}
@Override
public MappingBuilder map(String label, Function> getSources,
Function> getTargets) {
return this.registerBuilder(new MappingBuilderImpl<>(label, getSources, getTargets));
}
@Override
public ClassBuilder clazz(String label) {
return this.registerBuilder(new ClassBuilderImpl<>(label, this.osgiBridge, this.getParentShell()));
}
@Override
public ResourceBuilder resource(String label) {
return this.registerBuilder(new ResourceBuilderImpl<>(label, this.osgiBridge, this.getParentShell()));
}
@Override
public void validate(ValueValidator validator) {
this.validator = validator;
}
@Override
public void error(ErrorListener errorListener) {
this.errorListener = errorListener;
}
@Override
public void apply(String label, Applier applier) {
if ((label == null) || (label.trim().length() == 0)) {
label = "Apply"; // ensure have label
}
this.applierLabel = label;
this.applier = applier;
}
@Override
public void close(CloseListener closeListener) {
this.closeListener = closeListener;
}
/**
* Lists some of the {@link ValueRenderer} instances.
*/
private static class ValueLister implements ValueRendererContext, Configuration {
/**
* Model.
*/
private final M model;
/**
* {@link ValueValidator} for the model.
*/
private final ValueValidator modelValidator;
/**
* Configuration {@link Node}.
*/
private final Node configuartionNode;
/**
* Title.
*/
private final String title;
/**
* {@link GridPane}.
*/
private final GridPane grid;
/**
* Indicates whether configuration is dirty.
*/
private final Property dirtyProperty;
/**
* Indicates whether configuration is valid.
*/
private final Property validProperty;
/**
* {@link ErrorListener}.
*/
private final ErrorListener errorListener;
/**
* {@link Actioner}.
*/
private final Actioner actioner;
/**
* {@link Input} instances for this {@link ValueLister}.
*/
private final List> inputs = new LinkedList<>();
/**
* Indicates if organised for wide not narrow view.
*/
private boolean isWideNotNarrow = false;
/**
* Row index for organising this list.
*/
private int rowIndex;
/**
* Previous {@link ValueLister}.
*/
private final ValueLister prevLister;
/**
* Next {@link ValueLister}.
*/
private ValueLister nextLister = null;
/**
* Instantiate.
*
* @param model
* Model.
* @param modelValidator
* {@link ValueValidator} for the model. May be null.
* @param configurationNode
* Configuration {@link Node}.
* @param grid
* {@link GridPane}.
* @param title
* Title.
* @param dirtyProperty
* Dirty {@link Property}.
* @param validProperty
* Valid {@link Property}.
* @param errorListener
* {@link ErrorListener}.
* @param actioner
* {@link Actioner}.
* @param rowIndex
* Row index within wide view {@link GridPane} to continue rendering
* inputs.
* @param rendererFactories
* {@link ValueRendererFactory} instances.
* @param prevLister
* Previous {@link ValueLister}.
*/
@SuppressWarnings("unchecked")
public ValueLister(M model, ValueValidator modelValidator, Node configurationNode, GridPane grid,
String title, Property dirtyProperty, Property validProperty,
ErrorListener errorListener, Actioner actioner,
ValueRendererFactory[] rendererFactories, ValueLister prevLister) {
this.model = model;
this.modelValidator = modelValidator;
this.configuartionNode = configurationNode;
this.grid = grid;
this.dirtyProperty = dirtyProperty;
this.validProperty = validProperty;
this.errorListener = errorListener;
this.actioner = actioner;
this.title = title;
this.prevLister = prevLister;
// Ensure activate the inputs (once added)
List inputsToActivate = new ArrayList<>(rendererFactories.length * 2);
try {
// Render in the items
for (int i = 0; i < rendererFactories.length; i++) {
ValueRendererFactory rendererFactory = rendererFactories[i];
// Create the renderer
ValueRenderer renderer = (ValueRenderer) rendererFactory
.createValueRenderer(this);
// Obtain the value input
ValueInput valueInput = renderer.createInput();
inputsToActivate.add(valueInput);
// Add the input
Node valueInputNode = valueInput.getNode();
if (valueInputNode != null) {
grid.getChildren().add(valueInputNode);
}
// Add the label (if provided)
String labelText = renderer.getLabel(valueInput);
Node label = null;
if ((labelText == null) || (labelText.trim().length() == 0)) {
labelText = null; // no label
} else {
// Create the label
label = renderer.createLabel(labelText, valueInput);
if (label != null) {
grid.getChildren().add(label);
}
}
// Add the error feedback (if provided)
Node error = renderer.createErrorFeedback(valueInput);
if (error != null) {
grid.getChildren().add(error);
}
// Register the input
Input input = new Input<>(labelText, label, error, valueInputNode,
valueInput, renderer);
this.inputs.add(input);
grid.getChildren().add(input.spacing);
// Determine if choice value renderer
if (valueInput instanceof ChoiceValueInput) {
ChoiceValueInput choiceRenderer = (ChoiceValueInput) valueInput;
// Load choice
ValueRendererFactory[] splitRenderers = Arrays
.copyOfRange(rendererFactories, i + 1, rendererFactories.length);
Runnable loadChoice = () -> {
// Obtain the choice
Integer choice = choiceRenderer.getChoiceIndex().getValue();
if (choice == null) {
// No choice selected, so carry on with renderers
this.nextLister = new ValueLister<>(this.model, this.modelValidator, configurationNode,
grid, this.title, this.dirtyProperty, this.validProperty, this.errorListener,
this.actioner, splitRenderers, this);
} else {
// Have choice, so create concat list of remaining
ValueRendererFactory[] choiceRenderers = choiceRenderer
.getChoiceValueRendererFactories()[choice].get();
ValueRendererFactory[] remainingRenderers = new ValueRendererFactory[choiceRenderers.length
+ splitRenderers.length];
for (int c = 0; c < choiceRenderers.length; c++) {
remainingRenderers[c] = choiceRenderers[c];
}
for (int s = 0; s < splitRenderers.length; s++) {
remainingRenderers[choiceRenderers.length + s] = splitRenderers[s];
}
this.nextLister = new ValueLister<>(this.model, this.modelValidator, configurationNode,
grid, this.title, this.dirtyProperty, this.validProperty, this.errorListener,
this.actioner, remainingRenderers, this);
}
// Organise choice changes
if (this.isWideNotNarrow) {
this.organiseWide(this.rowIndex);
} else {
this.organiseNarrow(this.rowIndex);
}
// Refresh error (now different choice)
this.refreshError();
};
// Listen for changes in choice
choiceRenderer.getChoiceIndex().addListener((event) -> {
// Clear next listing (as change in choice)
if (this.nextLister != null) {
this.nextLister.removeControls();
this.nextLister = null;
}
// Load new choice
loadChoice.run();
});
// Load current choice
loadChoice.run();
// Stop loading renderers (as next listing will render)
return;
}
}
} finally {
// Activate the inputs
for (ValueInput input : inputsToActivate) {
input.activate();
}
}
}
/**
* Organises for wide view.
*
* @param rowIndex
* Row index to start organising inputs.
*/
private void organiseWide(int rowIndex) {
this.rowIndex = rowIndex;
// Organise the inputs
for (Input input : this.inputs) {
if (input.label != null) {
GridPane.setConstraints(input.label, 1, rowIndex, 1, 1, HPos.LEFT, VPos.CENTER, Priority.SOMETIMES,
Priority.ALWAYS);
}
if (input.errorFeedback != null) {
GridPane.setConstraints(input.errorFeedback, 2, rowIndex, 1, 1, HPos.CENTER, VPos.CENTER,
Priority.SOMETIMES, Priority.ALWAYS);
}
if (input.input != null) {
GridPane.setConstraints(input.input, 3, rowIndex, 1, 1, HPos.LEFT, VPos.TOP, Priority.ALWAYS,
Priority.ALWAYS);
}
GridPane.setConstraints(input.spacing, 4, rowIndex, 1, 1, HPos.LEFT, VPos.TOP, Priority.NEVER,
Priority.NEVER);
// Increment for next row
rowIndex += 2;
}
// Ensure next lister is also organised
if (this.nextLister != null) {
this.nextLister.organiseWide(rowIndex);
}
// Indicate now wide view
this.grid.getStyleClass().remove(CSS_CLASS_NARROW);
this.grid.getStyleClass().add(CSS_CLASS_WIDE);
this.isWideNotNarrow = true;
}
/**
* Organises for narrow view.
*
* @param rowIndex
* Row index to start organising inputs.
*/
private void organiseNarrow(int rowIndex) {
this.rowIndex = rowIndex;
// Organise the inputs
for (Input input : this.inputs) {
if (input.label != null) {
GridPane.setConstraints(input.label, 1, rowIndex, 1, 1, HPos.LEFT, VPos.TOP, Priority.ALWAYS,
Priority.ALWAYS);
}
if ((input.errorFeedback != null) && (input.input != null)) {
rowIndex++;
if (input.errorFeedback != null) {
GridPane.setConstraints(input.errorFeedback, 0, rowIndex, 1, 1, HPos.RIGHT, VPos.CENTER,
Priority.SOMETIMES, Priority.ALWAYS);
}
if (input.input != null) {
// Provide input on next row
GridPane.setConstraints(input.input, 1, rowIndex, 2, 1, HPos.LEFT, VPos.TOP, Priority.ALWAYS,
Priority.ALWAYS);
}
}
// Increment for next row
rowIndex++;
GridPane.setConstraints(input.spacing, 1, rowIndex, 2, 1, HPos.LEFT, VPos.TOP, Priority.NEVER,
Priority.ALWAYS);
rowIndex++;
}
// Ensure next lister is also organised
if (this.nextLister != null) {
this.nextLister.organiseNarrow(rowIndex);
}
// Indicate now narrow view
this.grid.getStyleClass().remove(CSS_CLASS_WIDE);
this.grid.getStyleClass().add(CSS_CLASS_NARROW);
this.isWideNotNarrow = false;
}
/**
* Removes the controls from view.
*/
private void removeControls() {
for (Input input : this.inputs) {
if (input.label != null) {
this.grid.getChildren().remove(input.label);
}
if (input.errorFeedback != null) {
this.grid.getChildren().remove(input.errorFeedback);
}
if (input.input != null) {
this.grid.getChildren().remove(input.input);
}
this.grid.getChildren().remove(input.spacing);
}
}
/*
* ================== ValueRendererContext ====================
*/
@Override
public M getModel() {
return this.model;
}
@Override
public Actioner getOptionalActioner() {
return this.actioner;
}
@Override
public ErrorListener getErrorListener() {
return this.errorListener;
}
@Override
public void reload(Builder, ?, ?> builder) {
// Obtain the first value lister
ValueLister firstValueLister = this;
while (firstValueLister.prevLister != null) {
firstValueLister = firstValueLister.prevLister;
}
// Search for the builder
ValueLister lister = firstValueLister;
while (lister != null) {
for (Input input : lister.inputs) {
input.renderer.reloadIf(builder);
}
lister = lister.nextLister;
}
}
/**
* Ensure do not recursively refresh the error.
*/
private boolean isRefreshingError = false;
@Override
@SuppressWarnings("unchecked")
public void refreshError() {
// Obtain the first value lister
ValueLister firstValueLister = this;
while (firstValueLister.prevLister != null) {
firstValueLister = firstValueLister.prevLister;
}
// Ensure not recursively refresh error
if (firstValueLister.isRefreshingError) {
return;
}
// Refresh the error
firstValueLister.isRefreshingError = true;
try {
// Search for first error
InputError errorInput = firstValueLister.getFirstError();
// If valid, undertake validation of model
if ((errorInput == null) && (this.modelValidator != null)) {
try {
// Validate the model
InputError[] validateError = new InputError[] { null };
this.modelValidator.validate(new ValueValidatorContext() {
@Override
public M getModel() {
return ValueLister.this.model;
}
@Override
public ReadOnlyProperty getValue() {
return new SimpleObjectProperty<>(ValueLister.this.model);
}
@Override
public void setError(String message) {
validateError[0] = new InputError<>(null, new MessageOnlyException(message));
}
@Override
public void reload(Builder, ?, ?> builder) {
ValueLister.this.reload(builder);
}
});
errorInput = validateError[0];
} catch (Throwable ex) {
// Provide failure error
errorInput = new InputError<>(null, ex);
}
}
// Indicate if valid
this.validProperty.setValue(errorInput == null);
// Determine if error
if (errorInput == null) {
this.errorListener.valid();
return; // no error
}
// Provide appropriate error
String label = errorInput.input != null ? errorInput.input.labelText : null;
if (errorInput.error instanceof MessageOnlyException) {
this.errorListener.error(label, errorInput.error.getMessage());
} else {
this.errorListener.error(label, errorInput.error);
}
} finally {
firstValueLister.isRefreshingError = false;
}
}
/**
* Obtains the first {@link ValueRenderer} error.
*
* @return First {@link ValueRenderer} error or null
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private InputError getFirstError() {
// Search through for the first error
for (Input input : this.inputs) {
// Determine if first error
Throwable error = input.renderer.getError(input.valueInput);
if (error != null) {
return new InputError<>(input, error);
}
}
// No error, so determine error in next list
return (this.nextLister != null) ? this.nextLister.getFirstError() : null;
}
/*
* ============ Configuration =====================
*/
@Override
public Node getConfigurationNode() {
return this.configuartionNode;
}
@Override
public String getTitle() {
return this.title;
}
@Override
public Property dirtyProperty() {
return this.dirtyProperty;
}
@Override
public Property validProperty() {
return this.validProperty;
}
@Override
public Actioner getActioner() {
if (this.actioner == null) {
throw new IllegalStateException("No apply configuration for model " + this.model.getClass().getName());
}
return this.actioner;
}
}
/**
* {@link Actioner} implementation.
*/
private static class ActionerImpl implements Actioner {
/**
* Model.
*/
private final M model;
/**
* Label.
*/
private final String label;
/**
* Applier.
*/
private final Applier applier;
/**
* Dirty {@link Property}.
*/
private final Property dirtyProperty;
/**
* {@link CloseListener}.
*/
private final CloseListener closeListener;
/**
* {@link ErrorListener}.
*/
private ErrorListener errorListener;
/**
* Instantiate.
*
* @param model
* Model.
* @param label
* Label.
* @param applier
* {@link Applier}.
* @param dirtyProperty
* Dirty {@link Property}.
* @param closeListner
* {@link CloseListener}.
*/
private ActionerImpl(M model, String label, Applier applier, Property dirtyProperty,
CloseListener closeListner) {
this.model = model;
this.label = label;
this.applier = applier;
this.dirtyProperty = dirtyProperty;
this.closeListener = closeListner;
}
/*
* ============== Actioner ===================
*/
@Override
public String getLabel() {
return this.label;
}
@Override
public void action() {
// Apply the model (handle potential failure in applying)
try {
this.applier.apply(this.model);
} catch (MessageOnlyApplyException ex) {
this.errorListener.error(null, ex.getMessage());
return; // failed to apply, so do not carry on to close
} catch (Throwable ex) {
this.errorListener.error(null, ex);
return; // failed to apply, so do not carry on to close
}
// No longer dirty
this.dirtyProperty.setValue(false);
// Notify that applied
if (this.closeListener != null) {
this.closeListener.applied();
}
}
}
/**
* Input.
*/
private static class Input {
/**
* Label text for the input.
*/
private final String labelText;
/**
* Label for the input.
*/
private final Node label;
/**
* Error feedback for the input.
*/
private final Node errorFeedback;
/**
* {@link Node} to capture the input.
*/
private final Node input;
/**
* Used for spacing.
*/
private final Pane spacing = new Pane();
/**
* {@link ValueInput}.
*/
private final I valueInput;
/**
* {@link ValueRenderer}. As bindings are weak references, need strong reference
* to {@link ValueRenderer} to keep bindings active.
*/
private final ValueRenderer renderer;
/**
* Instantiate.
*
* @param labelText
* Label text for the input.
* @param label
* Label for the input.
* @param errorFeedback
* Error feedback for the input.
* @param input
* {@link Node} to capture the input.
* @param valueInput
* {@link ValueInput}.
* @param renderer
* {@link ValueRenderer}.
*/
private Input(String labelText, Node label, Node errorFeedback, Node input, I valueInput,
ValueRenderer renderer) {
this.labelText = labelText;
this.label = label;
this.errorFeedback = errorFeedback;
this.input = input;
this.valueInput = valueInput;
this.renderer = renderer;
}
}
/**
* Input error.
*/
private static class InputError {
/**
* {@link Input}.
*/
private final Input input;
/**
* {@link Throwable} error.
*/
private final Throwable error;
/**
* Instantiate.
*
* @param input
* {@link Input}.
* @param error
* {@link Throwable} error.
*/
private InputError(Input input, Throwable error) {
this.input = input;
this.error = error;
}
}
/**
* Default {@link ErrorListener}.
*/
private static class DefaultErrorListener implements ErrorListener {
/**
* Header.
*/
private final GridPane header = new GridPane();
/**
* Valid header.
*/
private final HBox validHeader;
/**
* Error header.
*/
private final HBox errorHeader;
/**
* Error {@link Label}.
*/
private final Label errorLabel;
/**
* Toggle for showing the stack trace.
*/
private final HBox stackTraceToggle = new HBox();
/**
* Indicate if showing stack trace.
*/
private boolean isShowingStackTrace = false;
/**
* Stack trace.
*/
private final TextArea stackTrace = new TextArea();
/**
* Instantiate.
*
* @param title
* Title.
* @param actioner
* {@link Actioner}.
* @param grid
* {@link GridPane}.
* @param scroll
* {@link ScrollPane}.
* @param closeListener
* {@link CloseListener}.
* @param dirtyProperty
* Dirty {@link Property}.
* @param validProperty
* Valid {@link Property}.
*/
private DefaultErrorListener(String title, Actioner actioner, VBox wrapper, ScrollPane scroll,
CloseListener closeListener, Property dirtyProperty, ObservableBooleanValue validProperty) {
// Style the header
this.header.getStyleClass().setAll("header-panel", "dialog-pane", "button-bar", "configurer-header");
// Provide the error header
this.errorHeader = new HBox(10.0);
this.errorHeader.getStyleClass().setAll("error");
this.errorHeader.alignmentProperty().setValue(Pos.CENTER_LEFT);
this.errorHeader.setVisible(false);
GridPane.setHgrow(this.errorHeader, Priority.ALWAYS);
this.header.add(this.errorHeader, 0, 0);
// Provide the error header details
this.errorLabel = new Label();
this.errorHeader.getChildren().setAll(
new ImageView(new Image(DefaultImages.ERROR_IMAGE_PATH, 15, 15, true, true)), this.errorLabel);
// Provide the valid header
this.validHeader = new HBox(10.0);
this.validHeader.getStyleClass().setAll("valid");
this.validHeader.alignmentProperty().setValue(Pos.CENTER_LEFT);
GridPane.setHgrow(this.validHeader, Priority.ALWAYS);
this.header.add(this.validHeader, 0, 0);
// Provide valid header details
if (actioner != null) {
Button actionButton = new Button(actioner.getLabel());
actionButton.pseudoClassStateChanged(PseudoClass.getPseudoClass("default"), true);
actionButton.setOnAction((event) -> actioner.action());
actionButton.disableProperty().bind(Bindings.not(validProperty));
this.validHeader.getChildren().add(actionButton);
}
if (title != null) {
this.validHeader.getChildren().add(new Label(title));
}
// Stack trace not editable and fills area
this.stackTrace.setEditable(false);
this.stackTrace.prefHeightProperty().bind(scroll.prefHeightProperty());
this.stackTrace.prefWidthProperty().bind(wrapper.widthProperty());
// Provide stack trace toggle
Hyperlink stackTraceToggle = new Hyperlink();
stackTraceToggle.setText("Show Stack Trace");
stackTraceToggle.getStyleClass().setAll("details-button", "more");
stackTraceToggle.setOnAction((event) -> {
if (this.isShowingStackTrace) {
// Hide stack trace
wrapper.getChildren().setAll(this.header, scroll);
stackTraceToggle.setText("Show Stack Trace");
stackTraceToggle.getStyleClass().setAll("details-button", "more");
this.isShowingStackTrace = false;
} else {
// Show stack trace
wrapper.getChildren().setAll(this.header, this.stackTrace);
stackTraceToggle.setText("Hide Stack Trace");
stackTraceToggle.getStyleClass().setAll("details-button", "less");
this.isShowingStackTrace = true;
}
});
this.stackTraceToggle.getChildren().add(stackTraceToggle);
this.stackTraceToggle.getStyleClass().setAll("container");
this.stackTraceToggle.alignmentProperty().setValue(Pos.CENTER_RIGHT);
GridPane.setHgrow(this.stackTraceToggle, Priority.SOMETIMES);
this.header.add(this.stackTraceToggle, 1, 0);
// Determine if cancel
if (closeListener != null) {
// Provide cancel button
Button cancelButton = new Button("Cancel");
cancelButton.alignmentProperty().setValue(Pos.CENTER_RIGHT);
cancelButton.setOnAction((event) -> closeListener.cancelled());
cancelButton.setCancelButton(true);
GridPane.setHgrow(cancelButton, Priority.ALWAYS);
this.header.add(cancelButton, 2, 0);
}
}
/*
* ============= ErrorListener ====================
*/
@Override
public void error(String inputLabel, String message) {
// Display the error
this.errorLabel.setText((inputLabel == null ? "" : inputLabel + ": ") + message);
this.errorHeader.setVisible(true);
// No stack trace
this.stackTraceToggle.setVisible(false);
// Disallow applying
this.validHeader.setVisible(false);
}
@Override
public void error(String inputLabel, Throwable error) {
// Obtain the error message
String message = error.getMessage();
if ((message == null) || (message.trim().length() == 0)) {
message = error.getClass().getName();
}
// Display the error
this.error(inputLabel, message);
// Provide stack trace on tool tip
StringWriter buffer = new StringWriter();
error.printStackTrace(new PrintWriter(buffer));
this.stackTrace.setText(buffer.toString());
this.stackTraceToggle.setVisible(true);
}
@Override
public void valid() {
this.errorHeader.setVisible(false);
this.validHeader.setVisible(true);
this.stackTraceToggle.setVisible(false);
}
}
} © 2015 - 2025 Weber Informatics LLC | Privacy Policy