diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java b/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java
new file mode 100644
index 0000000..af1282a
--- /dev/null
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java
@@ -0,0 +1,631 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ * MODIFIED BY DORKBOX, LLC
+ */
+package dorkbox.util.javafx;
+
+import dorkbox.util.ScreenUtil;
+import impl.org.controlsfx.skin.NotificationBar;
+import javafx.animation.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.image.ImageView;
+import javafx.stage.Popup;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import javafx.util.Duration;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.tools.Utils;
+
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+
+/**
+ * An API to show popup notification messages to the user in the corner of their
+ * screen, unlike the {@link org.controlsfx.control.NotificationPane} which shows notification messages
+ * within your application itself.
+ *
+ *
Screenshot
+ *
+ * The following screenshot shows a sample notification rising from the
+ * bottom-right corner of my screen:
+ *
+ *
+ *
+ *
+ *
+ *
Code Example:
+ *
+ * To create the notification shown in the screenshot, simply do the following:
+ *
+ *
+ */
+public class Notifications {
+
+ /***************************************************************************
+ * * Static fields * *
+ **************************************************************************/
+
+ private static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$
+
+ /***************************************************************************
+ * * Private fields * *
+ **************************************************************************/
+
+ private String title;
+ private String text;
+ private Node graphic;
+ private ObservableList actions = FXCollections.observableArrayList();
+ private Pos position = Pos.BOTTOM_RIGHT;
+ private Duration hideAfterDuration = Duration.seconds(5);
+ private boolean hideCloseButton;
+ private EventHandler onAction;
+ private Window owner;
+
+ private List styleClass = new ArrayList<>();
+
+ /***************************************************************************
+ * * Constructors * *
+ **************************************************************************/
+
+ // we do not allow instantiation of the Notifications class directly - users
+ // must go via the builder API (that is, calling create())
+ private Notifications() {
+ // no-op
+ }
+
+ /***************************************************************************
+ * * Public API * *
+ **************************************************************************/
+
+ /**
+ * Call this to begin the process of building a notification to show.
+ */
+ public static
+ Notifications create() {
+ return new Notifications();
+ }
+
+ /**
+ * Specify the text to show in the notification.
+ */
+ public
+ Notifications text(String text) {
+ this.text = text;
+ return this;
+ }
+
+ /**
+ * Specify the title to show in the notification.
+ */
+ public
+ Notifications title(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Specify the graphic to show in the notification.
+ */
+ public
+ Notifications graphic(Node graphic) {
+ this.graphic = graphic;
+ return this;
+ }
+
+ /**
+ * Specify the position of the notification on screen, by default it is
+ * {@link Pos#BOTTOM_RIGHT bottom-right}.
+ */
+ public
+ Notifications position(Pos position) {
+ this.position = position;
+ return this;
+ }
+
+ /**
+ * The dialog window owner - if specified the notifications will be inside
+ * the owner, otherwise the notifications will be shown within the whole
+ * screen.
+ */
+ public
+ Notifications owner(Object owner) {
+ this.owner = Utils.getWindow(owner);
+ return this;
+ }
+
+ /**
+ * Specify the duration that the notification should show, after which it
+ * will be hidden.
+ */
+ public
+ Notifications hideAfter(Duration duration) {
+ this.hideAfterDuration = duration;
+ return this;
+ }
+
+ /**
+ * Specify what to do when the user clicks on the notification (in addition
+ * to the notification hiding, which happens whenever the notification is
+ * clicked on).
+ */
+ public
+ Notifications onAction(EventHandler onAction) {
+ this.onAction = onAction;
+ return this;
+ }
+
+ /**
+ * Specify that the notification should use the built-in dark styling,
+ * rather than the default 'modena' notification style (which is a
+ * light-gray).
+ */
+ public
+ Notifications darkStyle() {
+ styleClass.add(STYLE_CLASS_DARK);
+ return this;
+ }
+
+ /**
+ * Specify that the close button in the top-right corner of the notification
+ * should not be shown.
+ */
+ public
+ Notifications hideCloseButton() {
+ this.hideCloseButton = true;
+ return this;
+ }
+
+ /**
+ * Specify the actions that should be shown in the notification as buttons.
+ */
+ public
+ Notifications action(Action... actions) {
+ this.actions = actions == null ? FXCollections. observableArrayList() : FXCollections
+ .observableArrayList(actions);
+ return this;
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the
+ * built-in 'warning' graphic.
+ */
+ public void showWarning() {
+ graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-warning.png").toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the
+ * built-in 'information' graphic.
+ */
+ public void showInformation() {
+ graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-information.png").toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the
+ * built-in 'error' graphic.
+ */
+ public void showError() {
+ graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-error.png").toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the
+ * built-in 'confirm' graphic.
+ */
+ public void showConfirm() {
+ graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-confirm.png").toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown.
+ */
+ public void show() {
+ NotificationPopupHandler.getInstance().show(this);
+ }
+
+ /***************************************************************************
+ * * Private support classes * *
+ **************************************************************************/
+
+ // not public so no need for JavaDoc
+ private static final
+ class NotificationPopupHandler {
+
+ private static final NotificationPopupHandler INSTANCE = new NotificationPopupHandler();
+
+ static final
+ NotificationPopupHandler getInstance() {
+ return INSTANCE;
+ }
+ private final Map> popupsMap = new HashMap<>();
+ private final double padding = 15;
+
+ // for animating in the notifications
+ private final ParallelTransition parallelTransition = new ParallelTransition();
+
+ private double startX;
+ private double startY;
+ private double screenWidth;
+ private double screenHeight;
+
+ private boolean isShowing = false;
+
+ public
+ void show(Notifications notification) {
+ Window window;
+ final Window owner = notification.owner;
+ if (owner == null) {
+ Point mouseLocation = MouseInfo.getPointerInfo()
+ .getLocation();
+
+ GraphicsDevice deviceAtMouse = ScreenUtil.getGraphicsDeviceAt(mouseLocation);
+
+ final Rectangle screenBounds = deviceAtMouse.getDefaultConfiguration()
+ .getBounds();
+
+ /*
+ * If the owner is not set, we work with the whole screen.
+ * EDIT: we use the screen that the mouse is currently on.
+ */
+ startX = screenBounds.getX();
+ startY = screenBounds.getY();
+ screenWidth = screenBounds.getWidth();
+ screenHeight = screenBounds.getHeight();
+
+ window = Utils.getWindow(null);
+ }
+ else {
+ /*
+ * If the owner is set, we will make the notifications popup
+ * inside its window.
+ */
+ startX = owner.getX();
+ startY = owner.getY();
+ screenWidth = owner.getWidth();
+ screenHeight = owner.getHeight();
+ window = owner;
+ }
+ show(window, notification);
+ }
+
+ private void show(Window owner, final Notifications notification) {
+ // need to install our CSS
+ if (owner instanceof Stage) {
+ Scene ownerScene = ((Stage) owner).getScene();
+ ownerScene.getStylesheets().add(org.controlsfx.control.Notifications.class.getResource("notificationpopup.css")
+ .toExternalForm()); //$NON-NLS-1$
+ }
+
+ final Popup popup = new Popup();
+ popup.setAutoFix(false);
+
+ final Pos p = notification.position;
+
+ final NotificationBar notificationBar = new NotificationBar() {
+ @Override public String getTitle() {
+ return notification.title;
+ }
+
+ @Override public String getText() {
+ return notification.text;
+ }
+
+ @Override public Node getGraphic() {
+ return notification.graphic;
+ }
+
+ @Override public ObservableList getActions() {
+ return notification.actions;
+ }
+
+ @Override public boolean isShowing() {
+ return isShowing;
+ }
+
+ @Override protected double computeMinWidth(double height) {
+ String text = getText();
+ Node graphic = getGraphic();
+ if ((text == null || text.isEmpty()) && (graphic != null)) {
+ return graphic.minWidth(height);
+ }
+ return 400;
+ }
+
+ @Override protected double computeMinHeight(double width) {
+ String text = getText();
+ Node graphic = getGraphic();
+ if ((text == null || text.isEmpty()) && (graphic != null)) {
+ return graphic.minHeight(width);
+ }
+ return 100;
+ }
+
+ @Override public boolean isShowFromTop() {
+ return NotificationPopupHandler.this.isShowFromTop(notification.position);
+ }
+
+ @Override public void hide() {
+ isShowing = false;
+
+ // this would slide the notification bar out of view,
+ // but I prefer the fade out below
+ // doHide();
+
+ // animate out the popup by fading it
+ createHideTimeline(popup, this, p, Duration.ZERO).play();
+ }
+
+ @Override public boolean isCloseButtonVisible() {
+ return !notification.hideCloseButton;
+ }
+
+ @Override public double getContainerHeight() {
+ return startY + screenHeight;
+ }
+
+ @Override public void relocateInParent(double x, double y) {
+ // this allows for us to slide the notification upwards
+ switch (p) {
+ case BOTTOM_LEFT:
+ case BOTTOM_CENTER:
+ case BOTTOM_RIGHT:
+ popup.setAnchorY(y - padding);
+ break;
+ default:
+ // no-op
+ break;
+ }
+ }
+ };
+
+ notificationBar.getStyleClass().addAll(notification.styleClass);
+
+ notificationBar.setOnMouseClicked(e -> {
+ if (notification.onAction != null) {
+ ActionEvent actionEvent = new ActionEvent(notificationBar, notificationBar);
+ notification.onAction.handle(actionEvent);
+
+ // animate out the popup
+ createHideTimeline(popup, notificationBar, p, Duration.ZERO).play();
+ }
+ });
+
+ popup.getContent().add(notificationBar);
+ popup.show(owner, 0, 0);
+
+ // determine location for the popup
+ double anchorX = 0, anchorY = 0;
+ final double barWidth = notificationBar.getWidth();
+ final double barHeight = notificationBar.getHeight();
+
+ // get anchorX
+ switch (p) {
+ case TOP_LEFT:
+ case CENTER_LEFT:
+ case BOTTOM_LEFT:
+ anchorX = padding + startX;
+ break;
+
+ case TOP_CENTER:
+ case CENTER:
+ case BOTTOM_CENTER:
+ anchorX = startX + (screenWidth / 2.0) - barWidth / 2.0 - padding / 2.0;
+ break;
+
+ default:
+ case TOP_RIGHT:
+ case CENTER_RIGHT:
+ case BOTTOM_RIGHT:
+ anchorX = startX + screenWidth - barWidth - padding;
+ break;
+ }
+
+ // get anchorY
+ switch (p) {
+ case TOP_LEFT:
+ case TOP_CENTER:
+ case TOP_RIGHT:
+ anchorY = padding + startY;
+ break;
+
+ case CENTER_LEFT:
+ case CENTER:
+ case CENTER_RIGHT:
+ anchorY = startY + (screenHeight / 2.0) - barHeight / 2.0 - padding / 2.0;
+ break;
+
+ default:
+ case BOTTOM_LEFT:
+ case BOTTOM_CENTER:
+ case BOTTOM_RIGHT:
+ anchorY = startY + screenHeight - barHeight - padding;
+ break;
+ }
+
+ popup.setAnchorX(anchorX);
+ popup.setAnchorY(anchorY);
+
+ isShowing = true;
+
+ notificationBar.doShow();
+
+ addPopupToMap(p, popup);
+
+ // begin a timeline to get rid of the popup
+ Timeline timeline = createHideTimeline(popup, notificationBar, p, notification.hideAfterDuration);
+ timeline.play();
+ }
+
+ private void hide(Popup popup, Pos p) {
+ popup.hide();
+ removePopupFromMap(p, popup);
+ }
+
+ private Timeline createHideTimeline(final Popup popup, NotificationBar bar, final Pos p, Duration startDelay) {
+ KeyValue fadeOutBegin = new KeyValue(bar.opacityProperty(), 1.0);
+ KeyValue fadeOutEnd = new KeyValue(bar.opacityProperty(), 0.0);
+
+ KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin);
+ KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd);
+
+ Timeline timeline = new Timeline(kfBegin, kfEnd);
+ timeline.setDelay(startDelay);
+ timeline.setOnFinished(new EventHandler() {
+ @Override
+ public void handle(ActionEvent e) {
+ hide(popup, p);
+ }
+ });
+
+ return timeline;
+ }
+
+ private void addPopupToMap(Pos p, Popup popup) {
+ List popups;
+ if (!popupsMap.containsKey(p)) {
+ popups = new LinkedList<>();
+ popupsMap.put(p, popups);
+ } else {
+ popups = popupsMap.get(p);
+ }
+
+ doAnimation(p, popup);
+
+ // add the popup to the list so it is kept in memory and can be
+ // accessed later on
+ popups.add(popup);
+ }
+
+ private void removePopupFromMap(Pos p, Popup popup) {
+ if (popupsMap.containsKey(p)) {
+ List popups = popupsMap.get(p);
+ popups.remove(popup);
+ }
+ }
+
+ private void doAnimation(Pos p, Popup changedPopup) {
+ List popups = popupsMap.get(p);
+ if (popups == null) {
+ return;
+ }
+
+ final double newPopupHeight = changedPopup.getContent().get(0).getBoundsInParent().getHeight();
+
+ parallelTransition.stop();
+ parallelTransition.getChildren().clear();
+
+ final boolean isShowFromTop = isShowFromTop(p);
+
+ // animate all other popups in the list upwards so that the new one
+ // is in the 'new' area.
+ // firstly, we need to determine the target positions for all popups
+ double sum = 0;
+ double targetAnchors[] = new double[popups.size()];
+ for (int i = popups.size() - 1; i >= 0; i--) {
+ Popup _popup = popups.get(i);
+
+ final double popupHeight = _popup.getContent().get(0).getBoundsInParent().getHeight();
+
+ if (isShowFromTop) {
+ if (i == popups.size() - 1) {
+ sum = startY + newPopupHeight + padding;
+ } else {
+ sum += popupHeight;
+ }
+ targetAnchors[i] = sum;
+ } else {
+ if (i == popups.size() - 1) {
+ sum = changedPopup.getAnchorY() - popupHeight;
+ } else {
+ sum -= popupHeight;
+ }
+
+ targetAnchors[i] = sum;
+ }
+ }
+
+ // then we set up animations for each popup to animate towards the
+ // target
+ for (int i = popups.size() - 1; i >= 0; i--) {
+ final Popup _popup = popups.get(i);
+ final double anchorYTarget = targetAnchors[i];
+ if(anchorYTarget < 0){
+ _popup.hide();
+ }
+ final double oldAnchorY = _popup.getAnchorY();
+ final double distance = anchorYTarget - oldAnchorY;
+
+ Transition t = new Transition() {
+ {
+ setCycleDuration(Duration.millis(350));
+ }
+
+ @Override
+ protected void interpolate(double frac) {
+ double newAnchorY = oldAnchorY + distance * frac;
+ _popup.setAnchorY(newAnchorY);
+ }
+ };
+ t.setCycleCount(1);
+ parallelTransition.getChildren().add(t);
+ }
+ parallelTransition.play();
+ }
+
+ private boolean isShowFromTop(Pos p) {
+ switch (p) {
+ case TOP_LEFT:
+ case TOP_CENTER:
+ case TOP_RIGHT:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+}
+
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java
new file mode 100644
index 0000000..13135bf
--- /dev/null
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java
@@ -0,0 +1,914 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ * MODIFIED BY DORKBOX
+ */
+package dorkbox.util.javafx;
+
+import dorkbox.util.JavaFxUtil;
+import impl.org.controlsfx.ImplUtils;
+import javafx.application.Platform;
+import javafx.beans.property.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.ToolBar;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.*;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+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;
+
+/**
+ *
The API for creating multi-page Wizards, based on JavaFX {@link Dialog} API.
+ * Wizard can be setup in following few steps:
+ *
+ *
Design wizard pages by inheriting them from {@link WizardPane}
+ *
Define wizard flow by implementing {@link org.controlsfx.dialog.Wizard.Flow}
+ *
Create and instance of the Wizard and assign flow to it
+ *
Execute the wizard using showAndWait method
+ *
Values can be extracted from settings map by calling getSettings
+ *
+ *
For simple, linear wizards, the {@link LinearFlow} can be used.
+ * It is a flow based on a collection of wizard pages. Here is the example:
+ *
+ *
{@code // Create pages. Here for simplicity we just create and instance of WizardPane.
+ * WizardPane page1 = new WizardPane();
+ * WizardPane page2 = new WizardPane();
+ * WizardPane page2 = new WizardPane();
+ *
+ * // create wizard
+ * Wizard wizard = new Wizard();
+ *
+ * // create and assign the flow
+ * wizard.setFlow(new LinearFlow(page1, page2, page3));
+ *
+ * // show wizard and wait for response
+ * wizard.showAndWait().ifPresent(result -> {
+ * if (result == ButtonType.FINISH) {
+ * System.out.println("Wizard finished, settings: " + wizard.getSettings());
+ * }
+ * });}
+ *
+ *
For more complex wizard flows we suggest to create a custom ones, describing page traversal logic.
+ * Here is a simplified example:
+ *
+ *
{@code Wizard.Flow branchingFlow = new Wizard.Flow() {
+ * public Optional advance(WizardPane currentPage) {
+ * return Optional.of(getNext(currentPage));
+ * }
+ *
+ * public boolean canAdvance(WizardPane currentPage) {
+ * return currentPage != page3;
+ * }
+ *
+ * private WizardPane getNext(WizardPane currentPage) {
+ * if ( currentPage == null ) {
+ * return page1;
+ * } else if ( currentPage == page1) {
+ * // skipNextPage() does not exist - this just represents that you
+ * // can add a conditional statement here to change the page.
+ * return page1.skipNextPage()? page3: page2;
+ * } else {
+ * return page3;
+ * }
+ * }
+ * };}
+ */
+public class Wizard {
+ final Stage stage = new Stage(StageStyle.UTILITY);
+ private final Text headerText;
+ private final VBox center;
+
+ private final ObservableMap settings = FXCollections.observableHashMap();
+
+ final Stack pageHistory = new Stack<>();
+ Optional currentPage = Optional.empty();
+
+ private final BooleanProperty invalidProperty = new SimpleBooleanProperty(false);
+
+
+ // Read settings activated by default for backward compatibility
+ private final BooleanProperty readSettingsProperty = new SimpleBooleanProperty(true);
+
+ volatile boolean success = false;
+
+ private final Button BUTTON_PREVIOUS = new Button("Previous");
+
+ private final Button BUTTON_NEXT = new Button("Next");
+ private final EventHandler BUTTON_NEXT_EVENT_HANDLER = event -> {
+ if (event instanceof KeyEvent) {
+ final KeyCode code = ((KeyEvent)event).getCode();
+ if (code == KeyCode.ENTER || code == KeyCode.SPACE) {
+ event.consume();
+ goNext();
+ }
+ } else {
+ event.consume();
+ goNext();
+ }
+ };
+
+ private final EventHandler BUTTON_FINISH_EVENT_HANDLER = event -> {
+ if (event instanceof KeyEvent) {
+ final KeyCode code = ((KeyEvent)event).getCode();
+ if (code == KeyCode.ENTER || code == KeyCode.SPACE) {
+ event.consume();
+ goFinish();
+ }
+ } else {
+ event.consume();
+ goFinish();
+ }
+ };
+
+ private final StringProperty titleProperty = new SimpleStringProperty();
+ private volatile boolean useSpecifiedSize = false;
+ private final PopOver popOver;
+ private Text errorText;
+ private Font defaultHeaderFont;
+
+
+
+ /**************************************************************************
+ *
+ * Constructors
+ *
+ **************************************************************************/
+
+ /**
+ * Creates an instance of the wizard without an owner.
+ */
+ public
+ Wizard() {
+ this(null);
+ }
+
+ /**
+ * Creates an instance of the wizard with the given owner.
+ * @param owner The object from which the owner window is deduced (typically
+ * this is a Node, but it may also be a Scene or a Stage).
+ */
+ public
+ Wizard(Object owner) {
+ this(owner, ""); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates an instance of the wizard with the given owner and title.
+ *
+ * @param owner The object from which the owner window is deduced (typically
+ * this is a Node, but it may also be a Scene or a Stage).
+ * @param title The wizard title.
+ */
+ public
+ Wizard(Object owner, String title) {
+ BUTTON_PREVIOUS.setDisable(true);
+ BUTTON_NEXT.setDisable(true);
+
+ BUTTON_PREVIOUS.setId("prev-button");
+ BUTTON_NEXT.setId("next-button");
+
+ BUTTON_PREVIOUS.addEventFilter(ActionEvent.ACTION, event -> {
+ event.consume();
+ goPrev();
+ });
+
+ BUTTON_PREVIOUS.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
+ final KeyCode code = event.getCode();
+ if (code == KeyCode.ENTER || code == KeyCode.SPACE) {
+ event.consume();
+ goPrev();
+ }
+ });
+
+
+ stage.titleProperty()
+ .bind(this.titleProperty);
+ setTitle(title);
+
+ popOver = new PopOver();
+ popOver.setDetachable(false);
+ popOver.setDetached(false);
+ popOver.setAutoHide(false);
+
+ popOver.setArrowSize(12);
+ popOver.setArrowIndent(12);
+ popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
+ popOver.setCornerRadius(6);
+
+ VBox content = new VBox();
+ content.setPadding(new Insets(10));
+
+ errorText = new Text();
+ errorText.setFont(new Font(13));
+
+ content.setPadding(new Insets(20, 10, 0, 10));
+ content.getChildren()
+ .add(errorText);
+
+ popOver.setContentNode(content);
+
+ invalidProperty.addListener((o, ov, nv) -> validateActionState());
+
+ BorderPane borderPane = new BorderPane();
+
+ // Auto-sizing spacer
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+
+ Button cancel = new Button("Cancel");
+ cancel.addEventFilter(ActionEvent.ACTION, event -> {
+ event.consume();
+ success = false;
+ stage.close();
+ });
+
+
+ ToolBar toolbar = new ToolBar(cancel, spacer, BUTTON_PREVIOUS, BUTTON_NEXT);
+ toolbar.setPadding(new Insets(8));
+ borderPane.setBottom(toolbar);
+
+ headerText = new Text();
+ defaultHeaderFont = new Font(25);
+ headerText.setFont(defaultHeaderFont);
+
+ ToolBar region = new ToolBar(headerText);
+ region.setPadding(new Insets(15, 12, 15, 12));
+ borderPane.setTop(region);
+
+ center = new VBox();
+ borderPane.setCenter(center);
+
+ Scene scene2 = new Scene(borderPane);
+ stage.setScene(scene2);
+ stage.setMinWidth(300);
+ stage.setMinHeight(140);
+ stage.setWidth(300);
+ stage.setHeight(140);
+
+ Window window = null;
+ if (owner instanceof Window) {
+ window = (Window) owner;
+ }
+ else if (owner instanceof Node) {
+ window = ((Node) owner).getScene()
+ .getWindow();
+ }
+
+ stage.initOwner(window);
+ }
+
+ /**************************************************************************
+ *
+ * Public API
+ *
+ **************************************************************************/
+
+
+ /**
+ * Allows you to customize the width and height of the wizard. Must be set before the flow is set
+ */
+ public final
+ void setSize(int width, int height) {
+ stage.setWidth(width);
+ stage.setHeight(height);
+ useSpecifiedSize = true;
+ }
+
+
+ /**
+ * Shows the wizard but does not wait for a user response (in other words, this brings up a non-blocking dialog).
+ */
+ public final
+ void show() {
+ JavaFxUtil.showOnSameScreenAsMouseCenter(stage);
+ stage.show();
+ }
+
+ /**
+ * Shows the wizard and waits for the user response (in other words, brings up a blocking dialog, with the returned value (true=finished
+ * or false=cancel/close)
+ *
+ * @return An true/false depending on how they closed the wizard.
+ */
+ public final
+ boolean showAndWait() {
+ JavaFxUtil.showOnSameScreenAsMouseCenter(stage);
+ stage.showAndWait();
+
+ return success;
+ }
+
+ /**
+ * The settings map is the place where all data from pages is kept once the user moves on from the page, assuming there is a {@link
+ * ValueExtractor} that is capable of extracting a value out of the various fields on the page.
+ */
+ public final
+ ObservableMap getSettings() {
+ return settings;
+ }
+
+ /**
+ * Goes to the next page, or finishes the wizard
+ */
+ public
+ void goNext() {
+ currentPage.ifPresent(pageHistory::push);
+ currentPage = getFlow().advance(currentPage.orElse(null));
+ updatePage(stage, true);
+ }
+
+ private
+ void goPrev() {
+ currentPage = Optional.ofNullable(pageHistory.isEmpty() ? null : pageHistory.pop());
+ updatePage(stage, false);
+ }
+
+ private
+ void goFinish() {
+ success = true;
+ stage.close();
+ }
+
+ /**************************************************************************
+ *
+ * Properties
+ *
+ **************************************************************************/
+
+ // --- title
+
+ /**
+ * Return the titleProperty of the wizard.
+ */
+ public final
+ StringProperty titleProperty() {
+ return titleProperty;
+ }
+
+ /**
+ * Return the title of the wizard.
+ */
+ public final
+ String getTitle() {
+ return titleProperty.get();
+ }
+
+ /**
+ * Change the Title of the wizard.
+ */
+ public final
+ void setTitle(String title) {
+ titleProperty.set(title);
+ }
+
+ // --- flow
+ /**
+ * The {@link Flow} property represents the flow of pages in the wizard.
+ */
+ private final
+ ObjectProperty flow = new SimpleObjectProperty(new LinearFlow()) {
+ @Override
+ protected
+ void invalidated() {
+ updatePage(stage, false);
+ }
+
+ @Override
+ public
+ void set(Flow flow) {
+ super.set(flow);
+ pageHistory.clear();
+
+ if (flow != null) {
+ currentPage = flow.advance(currentPage.orElse(null));
+ updatePage(stage, true);
+ }
+ }
+ };
+
+ public final
+ ObjectProperty flowProperty() {
+ return flow;
+ }
+
+ /**
+ * Returns the currently set {@link Flow}, which represents the flow of pages in the wizard.
+ */
+ public final
+ Flow getFlow() {
+ return flow.get();
+ }
+
+ /**
+ * Sets the {@link Flow}, which represents the flow of pages in the wizard.
+ */
+ public final
+ void setFlow(Flow flow) {
+ this.flow.set(flow);
+ }
+
+
+ // --- Properties
+ private static final Object USER_DATA_KEY = new Object();
+
+ // A map containing a set of properties for this Wizard
+ private ObservableMap