From dadda62cabab7998f39328c3f41f7cb5d18bb8c0 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 8 Aug 2015 21:13:23 +0200 Subject: [PATCH] better support of invalid pane (using binding), added graphic to header text --- .../src/dorkbox/util/javafx/Wizard.java | 128 +++++++++++------- .../src/dorkbox/util/javafx/WizardPane.java | 101 +++++++++----- 2 files changed, 146 insertions(+), 83 deletions(-) diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java index 13135bf..dca6115 100644 --- a/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java +++ b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java @@ -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; /** *

The API for creating multi-page Wizards, based on JavaFX {@link Dialog} API.
@@ -128,6 +129,7 @@ public class Wizard { Optional 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 consumer = new Consumer() { + @Override + public + void accept(final WizardPane currentPage) { + if (currentPage.autoFocusNext) { + Platform.runLater(BUTTON_NEXT::requestFocus); + } + } + }; + invalidProperty.addListener((ObservableValue 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 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 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 diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/WizardPane.java b/Dorkbox-Util/src/dorkbox/util/javafx/WizardPane.java index 7d4372e..a0f21fc 100644 --- a/Dorkbox-Util/src/dorkbox/util/javafx/WizardPane.java +++ b/Dorkbox-Util/src/dorkbox/util/javafx/WizardPane.java @@ -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 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 o, ValidationResult ov, ValidationResult nv) -> { + final Collection 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 validator) { this.validationSupport.registerValidator(control, validator); } + + /** + * Necessary for enabling the "Next/Finish" button when a task is complete. + */ + public + void bind(final ObservableValue 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; + } }