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 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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue