better support of invalid pane (using binding), added graphic to header text
This commit is contained in:
parent
32cbef395f
commit
dadda62cab
|
@ -33,6 +33,7 @@ import dorkbox.util.JavaFxUtil;
|
||||||
import impl.org.controlsfx.ImplUtils;
|
import impl.org.controlsfx.ImplUtils;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.ObservableMap;
|
import javafx.collections.ObservableMap;
|
||||||
|
@ -55,11 +56,11 @@ import javafx.stage.StageStyle;
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
import org.controlsfx.control.PopOver;
|
import org.controlsfx.control.PopOver;
|
||||||
import org.controlsfx.tools.ValueExtractor;
|
import org.controlsfx.tools.ValueExtractor;
|
||||||
import org.controlsfx.validation.ValidationMessage;
|
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BooleanSupplier;
|
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/>
|
* <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();
|
Optional<WizardPane> currentPage = Optional.empty();
|
||||||
|
|
||||||
private final BooleanProperty invalidProperty = new SimpleBooleanProperty(false);
|
private final BooleanProperty invalidProperty = new SimpleBooleanProperty(false);
|
||||||
|
private final StringProperty invalidPropertyStrings = new SimpleStringProperty();
|
||||||
|
|
||||||
|
|
||||||
// Read settings activated by default for backward compatibility
|
// Read settings activated by default for backward compatibility
|
||||||
|
@ -166,9 +168,11 @@ public class Wizard {
|
||||||
|
|
||||||
private final StringProperty titleProperty = new SimpleStringProperty();
|
private final StringProperty titleProperty = new SimpleStringProperty();
|
||||||
private volatile boolean useSpecifiedSize = false;
|
private volatile boolean useSpecifiedSize = false;
|
||||||
|
|
||||||
private final PopOver popOver;
|
private final PopOver popOver;
|
||||||
private Text errorText;
|
private final Text popOverErrorText;
|
||||||
private Font defaultHeaderFont;
|
private final Font defaultHeaderFont;
|
||||||
|
private VBox graphicRegion;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,16 +246,35 @@ public class Wizard {
|
||||||
VBox content = new VBox();
|
VBox content = new VBox();
|
||||||
content.setPadding(new Insets(10));
|
content.setPadding(new Insets(10));
|
||||||
|
|
||||||
errorText = new Text();
|
popOverErrorText = new Text();
|
||||||
errorText.setFont(new Font(13));
|
popOverErrorText.setFont(new Font(12));
|
||||||
|
|
||||||
content.setPadding(new Insets(20, 10, 0, 10));
|
content.setPadding(new Insets(20, 10, 0, 10));
|
||||||
content.getChildren()
|
content.getChildren()
|
||||||
.add(errorText);
|
.add(popOverErrorText);
|
||||||
|
|
||||||
popOver.setContentNode(content);
|
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();
|
BorderPane borderPane = new BorderPane();
|
||||||
|
|
||||||
|
@ -276,11 +299,16 @@ public class Wizard {
|
||||||
defaultHeaderFont = new Font(25);
|
defaultHeaderFont = new Font(25);
|
||||||
headerText.setFont(defaultHeaderFont);
|
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));
|
region.setPadding(new Insets(15, 12, 15, 12));
|
||||||
borderPane.setTop(region);
|
borderPane.setTop(region);
|
||||||
|
|
||||||
center = new VBox();
|
center = new VBox();
|
||||||
|
center.setMinSize(0, 0);
|
||||||
|
center.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||||
|
// center.setStyle("-fx-background-color: #2046ff;");
|
||||||
borderPane.setCenter(center);
|
borderPane.setCenter(center);
|
||||||
|
|
||||||
Scene scene2 = new Scene(borderPane);
|
Scene scene2 = new Scene(borderPane);
|
||||||
|
@ -302,6 +330,25 @@ public class Wizard {
|
||||||
stage.initOwner(window);
|
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
|
* Public API
|
||||||
|
@ -611,6 +658,9 @@ public class Wizard {
|
||||||
// based on the settings it has received
|
// based on the settings it has received
|
||||||
page.onExitingPage(this);
|
page.onExitingPage(this);
|
||||||
|
|
||||||
|
invalidProperty.unbind();
|
||||||
|
invalidPropertyStrings.unbind();
|
||||||
|
|
||||||
invalidProperty.set(false);
|
invalidProperty.set(false);
|
||||||
popOver.hide();
|
popOver.hide();
|
||||||
});
|
});
|
||||||
|
@ -632,6 +682,15 @@ public class Wizard {
|
||||||
// then give user a chance to modify the default actions
|
// then give user a chance to modify the default actions
|
||||||
currentPage.onEnteringPage(this);
|
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;
|
final Node firstFocusElement = currentPage.firstFocusElement;
|
||||||
if (firstFocusElement != null) {
|
if (firstFocusElement != null) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
@ -659,20 +718,25 @@ public class Wizard {
|
||||||
else {
|
else {
|
||||||
headerText.setFont(defaultHeaderFont);
|
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();
|
ObservableList<Node> children = center.getChildren();
|
||||||
children.clear();
|
children.clear();
|
||||||
children.add(currentPage.getContent());
|
children.add(currentPage.anchorPane);
|
||||||
|
|
||||||
|
|
||||||
if (!useSpecifiedSize) {
|
if (!useSpecifiedSize) {
|
||||||
currentPage.getContent()
|
currentPage.anchorPane.autosize();
|
||||||
.autosize();
|
|
||||||
stage.sizeToScene();
|
stage.sizeToScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPage.validationSupport.redecorate();
|
currentPage.validationSupport.redecorate();
|
||||||
notifyValidationChange(currentPage, currentPage.validationErrors);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
validateActionState();
|
validateActionState();
|
||||||
|
@ -713,7 +777,8 @@ public class Wizard {
|
||||||
private static
|
private static
|
||||||
void validateButton(Button button, BooleanSupplier condition) {
|
void validateButton(Button button, BooleanSupplier condition) {
|
||||||
if ( button != null ) {
|
if ( button != null ) {
|
||||||
button.setDisable(condition.getAsBoolean());
|
boolean asBoolean = condition.getAsBoolean();
|
||||||
|
button.setDisable(asBoolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,13 +787,13 @@ public class Wizard {
|
||||||
private
|
private
|
||||||
void readSettings(WizardPane page) {
|
void readSettings(WizardPane page) {
|
||||||
// for now we cannot know the structure of the page, so we just drill down
|
// 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
|
// to the leaf nodes. We stop only if we find a node that is a
|
||||||
// ValueContainer (either by implementing the interface), or being
|
// ValueContainer (either by implementing the interface), or being
|
||||||
// listed in the internal valueContainers map.
|
// listed in the internal valueContainers map.
|
||||||
|
|
||||||
settingCounter = 0;
|
settingCounter = 0;
|
||||||
checkNode(page.getContent());
|
checkNode(page.anchorPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -793,37 +858,6 @@ public class Wizard {
|
||||||
BUTTON_PREVIOUS.requestFocus();
|
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
|
* Support classes
|
||||||
|
|
|
@ -1,48 +1,70 @@
|
||||||
package dorkbox.util.javafx;
|
package dorkbox.util.javafx;
|
||||||
|
|
||||||
import dorkbox.util.JavaFxUtil;
|
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.Node;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.Control;
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.scene.control.DialogPane;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import org.controlsfx.validation.ValidationMessage;
|
import org.controlsfx.validation.ValidationMessage;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
import org.controlsfx.validation.Validator;
|
import org.controlsfx.validation.Validator;
|
||||||
|
|
||||||
import java.util.Collection;
|
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
|
* 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.
|
* methods related to {@link #onEnteringPage(Wizard) entering} and {@link #onExitingPage(Wizard) exiting} the page.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("UnusedParameters")
|
||||||
public
|
public
|
||||||
class WizardPane {
|
class WizardPane {
|
||||||
|
|
||||||
String headerText;
|
String headerText;
|
||||||
Font headerFont;
|
Font headerFont;
|
||||||
|
Node headerGraphic;
|
||||||
|
|
||||||
AnchorPane content = new AnchorPane();
|
Node anchorPane;
|
||||||
|
|
||||||
Node firstFocusElement;
|
Node firstFocusElement;
|
||||||
|
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
final StringProperty invalidPropertyStrings = new SimpleStringProperty();
|
||||||
volatile Collection<ValidationMessage> validationErrors = Collections.emptyList();
|
final BooleanProperty invalidProperty = new SimpleBooleanProperty();
|
||||||
|
final ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
boolean autoFocusNext = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of wizard pane.
|
* Creates an instance of wizard pane.
|
||||||
* @param wizard necessary for validation support, to notify the wizard when this page becomes valid
|
|
||||||
*/
|
*/
|
||||||
public
|
public
|
||||||
WizardPane(final Wizard wizard) {
|
WizardPane() {
|
||||||
validationSupport.validationResultProperty()
|
validationSupport.validationResultProperty()
|
||||||
.addListener((o, ov, nv) -> {
|
.addListener((ObservableValue<? extends ValidationResult> o, ValidationResult ov, ValidationResult nv) -> {
|
||||||
validationErrors = nv.getErrors();
|
final Collection<ValidationMessage> errors = nv.getErrors();
|
||||||
wizard.notifyValidationChange(this, 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
|
public
|
||||||
void setContent(final Region content) {
|
void setContent(final Node content) {
|
||||||
content.setMinSize(0, 0);
|
// make this content fill the parent (which is a vbox)
|
||||||
|
VBox.setVgrow(content, Priority.ALWAYS);
|
||||||
AnchorPane.setTopAnchor(content, 0.0);
|
this.anchorPane = content;
|
||||||
AnchorPane.setRightAnchor(content, 0.0);
|
|
||||||
AnchorPane.setLeftAnchor(content, 0.0);
|
|
||||||
AnchorPane.setBottomAnchor(content, 0.0);
|
|
||||||
|
|
||||||
this.content.getChildren().setAll(content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
|
@ -93,28 +110,19 @@ class WizardPane {
|
||||||
text.setFont(JavaFxUtil.DEFAULT_FONT);
|
text.setFont(JavaFxUtil.DEFAULT_FONT);
|
||||||
text.setText(contentText);
|
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();
|
VBox region = new VBox();
|
||||||
region.getChildren().add(text);
|
region.setPadding(new Insets(6));
|
||||||
|
|
||||||
|
region.getChildren().add(text);
|
||||||
region.setMinSize(0, 0);
|
region.setMinSize(0, 0);
|
||||||
|
|
||||||
AnchorPane.setTopAnchor(region, 0.0);
|
// make this content fill the parent (which is a vbox)
|
||||||
AnchorPane.setRightAnchor(region, 0.0);
|
VBox.setVgrow(region, Priority.ALWAYS);
|
||||||
AnchorPane.setLeftAnchor(region, 0.0);
|
this.anchorPane = region;
|
||||||
AnchorPane.setBottomAnchor(region, 0.0);
|
|
||||||
|
|
||||||
this.content.getChildren().setAll(region);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
|
||||||
String getHeaderText() {
|
|
||||||
return headerText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
Region getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
public
|
||||||
void setFirstFocusElement(final Node firstFocusElement) {
|
void setFirstFocusElement(final Node firstFocusElement) {
|
||||||
|
@ -132,4 +140,25 @@ class WizardPane {
|
||||||
void registerValidator(final Control control, final Validator<Object> validator) {
|
void registerValidator(final Control control, final Validator<Object> validator) {
|
||||||
this.validationSupport.registerValidator(control, 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user