better support of invalid pane (using binding), added graphic to header text

This commit is contained in:
nathan 2015-08-08 21:13:23 +02:00
parent 32cbef395f
commit dadda62cab
2 changed files with 146 additions and 83 deletions

View File

@ -33,6 +33,7 @@ import dorkbox.util.JavaFxUtil;
import impl.org.controlsfx.ImplUtils;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
@ -55,11 +56,11 @@ import javafx.stage.StageStyle;
import javafx.stage.Window;
import org.controlsfx.control.PopOver;
import org.controlsfx.tools.ValueExtractor;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationSupport;
import java.util.*;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
/**
* <p>The API for creating multi-page Wizards, based on JavaFX {@link Dialog} API.<br/>
@ -128,6 +129,7 @@ public class Wizard {
Optional<WizardPane> currentPage = Optional.empty();
private final BooleanProperty invalidProperty = new SimpleBooleanProperty(false);
private final StringProperty invalidPropertyStrings = new SimpleStringProperty();
// Read settings activated by default for backward compatibility
@ -166,9 +168,11 @@ public class Wizard {
private final StringProperty titleProperty = new SimpleStringProperty();
private volatile boolean useSpecifiedSize = false;
private final PopOver popOver;
private Text errorText;
private Font defaultHeaderFont;
private final Text popOverErrorText;
private final Font defaultHeaderFont;
private VBox graphicRegion;
@ -242,16 +246,35 @@ public class Wizard {
VBox content = new VBox();
content.setPadding(new Insets(10));
errorText = new Text();
errorText.setFont(new Font(13));
popOverErrorText = new Text();
popOverErrorText.setFont(new Font(12));
content.setPadding(new Insets(20, 10, 0, 10));
content.getChildren()
.add(errorText);
.add(popOverErrorText);
popOver.setContentNode(content);
invalidProperty.addListener((o, ov, nv) -> validateActionState());
invalidPropertyStrings.addListener((observable, oldValue, newValue) -> {
validatePopover(newValue);
});
Consumer<WizardPane> consumer = new Consumer<WizardPane>() {
@Override
public
void accept(final WizardPane currentPage) {
if (currentPage.autoFocusNext) {
Platform.runLater(BUTTON_NEXT::requestFocus);
}
}
};
invalidProperty.addListener((ObservableValue<? extends Boolean> o, Boolean ov, Boolean nv) -> {
validateActionState();
// the value is "invalid", so we want "!invalid"
if (ov && !nv) {
currentPage.ifPresent(consumer);
}
});
BorderPane borderPane = new BorderPane();
@ -276,11 +299,16 @@ public class Wizard {
defaultHeaderFont = new Font(25);
headerText.setFont(defaultHeaderFont);
ToolBar region = new ToolBar(headerText);
graphicRegion = new VBox();
ToolBar region = new ToolBar(graphicRegion, headerText);
region.setPadding(new Insets(15, 12, 15, 12));
borderPane.setTop(region);
center = new VBox();
center.setMinSize(0, 0);
center.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
// center.setStyle("-fx-background-color: #2046ff;");
borderPane.setCenter(center);
Scene scene2 = new Scene(borderPane);
@ -302,6 +330,25 @@ public class Wizard {
stage.initOwner(window);
}
private
void validatePopover(final String newValue) {
if (newValue != null) {
currentPage.ifPresent(currentPage -> {
final PopOver popOver = this.popOver;
this.popOverErrorText.setText(newValue);
if (!popOver.isShowing()) {
popOver.setX(0);
popOver.setY(0);
popOver.show(BUTTON_NEXT, -10);
}
});
} else {
popOver.hide();
}
}
/**************************************************************************
*
* Public API
@ -611,6 +658,9 @@ public class Wizard {
// based on the settings it has received
page.onExitingPage(this);
invalidProperty.unbind();
invalidPropertyStrings.unbind();
invalidProperty.set(false);
popOver.hide();
});
@ -632,6 +682,15 @@ public class Wizard {
// then give user a chance to modify the default actions
currentPage.onEnteringPage(this);
invalidProperty.bind(currentPage.invalidProperty);
invalidPropertyStrings.bind(currentPage.invalidPropertyStrings);
if (currentPage.invalidProperty.get()) {
validatePopover(currentPage.invalidPropertyStrings.get());
} else {
popOver.hide();
}
final Node firstFocusElement = currentPage.firstFocusElement;
if (firstFocusElement != null) {
Platform.runLater(() -> {
@ -659,20 +718,25 @@ public class Wizard {
else {
headerText.setFont(defaultHeaderFont);
}
headerText.setText(currentPage.getHeaderText());
if (currentPage.headerGraphic != null) {
graphicRegion.getChildren().setAll(currentPage.headerGraphic);
} else {
graphicRegion.getChildren().clear();
}
headerText.setText(currentPage.headerText);
ObservableList<Node> children = center.getChildren();
children.clear();
children.add(currentPage.getContent());
children.add(currentPage.anchorPane);
if (!useSpecifiedSize) {
currentPage.getContent()
.autosize();
currentPage.anchorPane.autosize();
stage.sizeToScene();
}
currentPage.validationSupport.redecorate();
notifyValidationChange(currentPage, currentPage.validationErrors);
});
validateActionState();
@ -713,7 +777,8 @@ public class Wizard {
private static
void validateButton(Button button, BooleanSupplier condition) {
if ( button != null ) {
button.setDisable(condition.getAsBoolean());
boolean asBoolean = condition.getAsBoolean();
button.setDisable(asBoolean);
}
}
@ -722,13 +787,13 @@ public class Wizard {
private
void readSettings(WizardPane page) {
// for now we cannot know the structure of the page, so we just drill down
// through the entire scenegraph (from page.content down) until we get
// through the entire scenegraph (from page.anchorPane down) until we get
// to the leaf nodes. We stop only if we find a node that is a
// ValueContainer (either by implementing the interface), or being
// listed in the internal valueContainers map.
settingCounter = 0;
checkNode(page.getContent());
checkNode(page.anchorPane);
}
private
@ -793,37 +858,6 @@ public class Wizard {
BUTTON_PREVIOUS.requestFocus();
}
void notifyValidationChange(final WizardPane wizardPane, Collection<ValidationMessage> errors) {
if (currentPage.orElse(null) == wizardPane) {
Platform.runLater(() -> {
boolean hasErrors = !errors.isEmpty();
invalidProperty.set(hasErrors);
final PopOver popOver = this.popOver;
if (hasErrors) {
String errorText = errors.iterator()
.next()
.getText()
.trim();
this.errorText.setText(errorText);
if (!popOver.isShowing()) {
popOver.setX(0);
popOver.setY(0);
popOver.show(BUTTON_NEXT, -10);
}
}
else {
popOver.hide();
}
validateActionState();
});
}
}
/**************************************************************************
*
* Support classes

View File

@ -1,48 +1,70 @@
package dorkbox.util.javafx;
import dorkbox.util.JavaFxUtil;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.DialogPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import java.util.Collection;
import java.util.Collections;
/**
* WizardPane is the base class for all wizard pages. The API is essentially the {@link DialogPane}, with the addition of convenience
* methods related to {@link #onEnteringPage(Wizard) entering} and {@link #onExitingPage(Wizard) exiting} the page.
*/
@SuppressWarnings("UnusedParameters")
public
class WizardPane {
String headerText;
Font headerFont;
Node headerGraphic;
AnchorPane content = new AnchorPane();
Node anchorPane;
Node firstFocusElement;
ValidationSupport validationSupport = new ValidationSupport();
volatile Collection<ValidationMessage> validationErrors = Collections.emptyList();
final StringProperty invalidPropertyStrings = new SimpleStringProperty();
final BooleanProperty invalidProperty = new SimpleBooleanProperty();
final ValidationSupport validationSupport = new ValidationSupport();
boolean autoFocusNext = false;
/**
* Creates an instance of wizard pane.
* @param wizard necessary for validation support, to notify the wizard when this page becomes valid
*/
public
WizardPane(final Wizard wizard) {
WizardPane() {
validationSupport.validationResultProperty()
.addListener((o, ov, nv) -> {
validationErrors = nv.getErrors();
wizard.notifyValidationChange(this, nv.getErrors());
.addListener((ObservableValue<? extends ValidationResult> o, ValidationResult ov, ValidationResult nv) -> {
final Collection<ValidationMessage> errors = nv.getErrors();
final boolean empty = errors.isEmpty();
if (empty) {
invalidPropertyStrings.set(null);
} else {
String errorText = errors.iterator()
.next()
.getText()
.trim();
if (!errorText.equals(invalidPropertyStrings.get())) {
invalidPropertyStrings.set(errorText);
}
}
invalidProperty.set(!empty);
});
}
@ -76,15 +98,10 @@ class WizardPane {
}
public
void setContent(final Region content) {
content.setMinSize(0, 0);
AnchorPane.setTopAnchor(content, 0.0);
AnchorPane.setRightAnchor(content, 0.0);
AnchorPane.setLeftAnchor(content, 0.0);
AnchorPane.setBottomAnchor(content, 0.0);
this.content.getChildren().setAll(content);
void setContent(final Node content) {
// make this content fill the parent (which is a vbox)
VBox.setVgrow(content, Priority.ALWAYS);
this.anchorPane = content;
}
public
@ -93,28 +110,19 @@ class WizardPane {
text.setFont(JavaFxUtil.DEFAULT_FONT);
text.setText(contentText);
// we use a Vbox, so that the text starts at the topleft.
// if we used a stackpane, it would be centered in the parent node
VBox region = new VBox();
region.getChildren().add(text);
region.setPadding(new Insets(6));
region.getChildren().add(text);
region.setMinSize(0, 0);
AnchorPane.setTopAnchor(region, 0.0);
AnchorPane.setRightAnchor(region, 0.0);
AnchorPane.setLeftAnchor(region, 0.0);
AnchorPane.setBottomAnchor(region, 0.0);
this.content.getChildren().setAll(region);
// make this content fill the parent (which is a vbox)
VBox.setVgrow(region, Priority.ALWAYS);
this.anchorPane = region;
}
public
String getHeaderText() {
return headerText;
}
public
Region getContent() {
return content;
}
public
void setFirstFocusElement(final Node firstFocusElement) {
@ -132,4 +140,25 @@ class WizardPane {
void registerValidator(final Control control, final Validator<Object> validator) {
this.validationSupport.registerValidator(control, validator);
}
/**
* Necessary for enabling the "Next/Finish" button when a task is complete.
*/
public
void bind(final ObservableValue<Boolean> invalidProperty) {
this.invalidProperty.bind(invalidProperty);
}
/**
* Enables the wizard to automatically focus on the "Next" (or "Finish"), when this item is valid.
*/
public
void autoFocusNext() {
autoFocusNext = true;
}
public
void setHeaderGraphic(final Node headerGraphic) {
this.headerGraphic = headerGraphic;
}
}