All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.controlsfx.dialog.Wizard Maven / Gradle / Ivy

Go to download

High quality UI controls and other tools to complement the core JavaFX distribution

There is a newer version: 11.2.1
Show newest version
/**
 * Copyright (c) 2014, 2018 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.
 */
package org.controlsfx.dialog;

import static impl.org.controlsfx.i18n.Localization.asKey;
import static impl.org.controlsfx.i18n.Localization.localize;
import impl.org.controlsfx.ImplUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.function.BooleanSupplier;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.layout.Pane;
import javafx.stage.Screen;
import javafx.stage.Window;

import org.controlsfx.tools.ValueExtractor;
import org.controlsfx.validation.ValidationSupport;

/**
 * 

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 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 page3 = 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 { /************************************************************************** * * Private fields * **************************************************************************/ private Dialog dialog; private final ObservableMap settings = FXCollections.observableHashMap(); private final Stack pageHistory = new Stack<>(); private 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); private final ButtonType BUTTON_PREVIOUS = new ButtonType(localize(asKey("wizard.previous.button")), ButtonData.BACK_PREVIOUS); //$NON-NLS-1$ private final EventHandler BUTTON_PREVIOUS_ACTION_HANDLER = actionEvent -> { actionEvent.consume(); Optional nextPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.pop() ); updatePage(dialog, nextPage, false); }; private final ButtonType BUTTON_NEXT = new ButtonType(localize(asKey("wizard.next.button")), ButtonData.NEXT_FORWARD); //$NON-NLS-1$ private final EventHandler BUTTON_NEXT_ACTION_HANDLER = actionEvent -> { actionEvent.consume(); currentPage.ifPresent(page->pageHistory.push(page)); Optional nextPage = getFlow().advance(currentPage.orElse(null)); updatePage(dialog, nextPage, true); }; private final EventHandler BUTTON_FINISH_ACTION_HANDLER = actionEvent -> { updatePage(dialog, Optional.empty(), true); }; private final EventHandler BUTTON_CANCEL_ACTION_HANDLER = actionEvent -> { updatePage(dialog, Optional.empty(), false); }; private final StringProperty titleProperty = new SimpleStringProperty(); /************************************************************************** * * 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) { invalidProperty.addListener( (o, ov, nv) -> validateActionState()); dialog = new Dialog<>(); dialog.titleProperty().bind(this.titleProperty); setTitle(title); Window window = null; if ( owner instanceof Window) { window = (Window)owner; } else if ( owner instanceof Node ) { window = ((Node)owner).getScene().getWindow(); } dialog.initOwner(window); } /************************************************************************** * * Public API * **************************************************************************/ // /** // * Shows the wizard but does not wait for a user response (in other words, // * this brings up a non-blocking dialog). Users of this API must either // * poll the {@link #resultProperty() result property}, or else add a listener // * to the result property to be informed of when it is set. // */ // public final void show() { // dialog.show(); // } /** * Shows the wizard and waits for the user response (in other words, brings * up a blocking dialog, with the returned value the users input). * * @return An {@link Optional} that contains the result. */ public final Optional showAndWait() { return dialog.showAndWait(); } /** * @return {@link Dialog#resultProperty()} of the {@link Dialog} representing this {@link Wizard}. */ public final ObjectProperty resultProperty() { return dialog.resultProperty(); } /** * 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; } /************************************************************************** * * 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. * @param title */ public final void setTitle( String title ) { titleProperty.set(title); } // --- flow /** * The {@link Flow} property represents the flow of pages in the wizard. */ private ObjectProperty flow = new SimpleObjectProperty(new LinearFlow()) { @Override protected void invalidated() { updatePage(dialog, Optional.empty(), false); } @Override public void set(Flow flow) { super.set(flow); pageHistory.clear(); if ( flow != null ) { Optional nextPage = flow.advance(currentPage.orElse(null)); updatePage(dialog, nextPage, 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 properties; /** * Returns an observable map of properties on this Wizard for use primarily * by application developers - not to be confused with the * {@link #getSettings()} map that represents the values entered by the user * into the wizard. * * @return an observable map of properties on this Wizard for use primarily * by application developers */ public final ObservableMap getProperties() { if (properties == null) { properties = FXCollections.observableMap(new HashMap<>()); } return properties; } /** * Tests if this Wizard has properties. * @return true if this Wizard has properties. */ public boolean hasProperties() { return properties != null && !properties.isEmpty(); } // --- UserData /** * Convenience method for setting a single Object property that can be * retrieved at a later date. This is functionally equivalent to calling * the getProperties().put(Object key, Object value) method. This can later * be retrieved by calling {@link #getUserData()}. * * @param value The value to be stored - this can later be retrieved by calling * {@link #getUserData()}. */ public void setUserData(Object value) { getProperties().put(USER_DATA_KEY, value); } /** * Returns a previously set Object property, or null if no such property * has been set using the {@link #setUserData(Object)} method. * * @return The Object that was previously set, or null if no property * has been set or if null was set. */ public Object getUserData() { return getProperties().get(USER_DATA_KEY); } /** * Sets the value of the property {@code invalid}. * * @param invalid The new validation state * {@link #invalidProperty() } */ public final void setInvalid(boolean invalid) { invalidProperty.set(invalid); } /** * Gets the value of the property {@code invalid}. * * @return The validation state * @see #invalidProperty() */ public final boolean isInvalid() { return invalidProperty.get(); } /** * Property for overriding the individual validation state of this {@link Wizard}. * Setting {@code invalid} to true will disable the next/finish Button and the user * will not be able to advance to the next page of the {@link Wizard}. Setting * {@code invalid} to false will enable the next/finish Button.
*
* For example you can use the {@link ValidationSupport#invalidProperty()} of a * page and bind it to the {@code invalid} property:
* {@code * wizard.invalidProperty().bind(page.validationSupport.invalidProperty()); * } * * @return The validation state property */ public final BooleanProperty invalidProperty() { return invalidProperty; } /** * Sets the value of the property {@code readSettings}. * * @param readSettings The new read-settings state * @see #readSettingsProperty() */ public final void setReadSettings(boolean readSettings) { readSettingsProperty.set(readSettings); } /** * Gets the value of the property {@code readSettings}. * * @return The read-settings state * @see #readSettingsProperty() */ public final boolean isReadSettings() { return readSettingsProperty.get(); } /** * Property for overriding the individual read-settings state of this {@link Wizard}. * Setting {@code readSettings} to true will enable the value extraction for this * {@link Wizard}. Setting {@code readSettings} to false will disable the value * extraction for this {@link Wizard}. * * @return The readSettings state property */ public final BooleanProperty readSettingsProperty() { return readSettingsProperty; } /************************************************************************** * * Private implementation * **************************************************************************/ private void updatePage(Dialog dialog, Optional nextPage, boolean advancing) { Flow flow = getFlow(); if (flow == null) { return; } Optional prevPage = currentPage; prevPage.ifPresent( page -> { // if we are going forward in the wizard, we read in the settings // from the page and store them in the settings map. // If we are going backwards, we do nothing // This is only performed if readSettings is true. if (advancing && isReadSettings()) { readSettings(page); } // give the previous wizard page a chance to update the pages list // based on the settings it has received page.onExitingPage(this); }); currentPage = nextPage; currentPage.ifPresent(currentPage -> { // put in default actions addButtonIfMissing(currentPage, BUTTON_PREVIOUS, BUTTON_PREVIOUS_ACTION_HANDLER); addButtonIfMissing(currentPage, BUTTON_NEXT, BUTTON_NEXT_ACTION_HANDLER); addButtonIfMissing(currentPage, ButtonType.FINISH, BUTTON_FINISH_ACTION_HANDLER); addButtonIfMissing(currentPage, ButtonType.CANCEL, BUTTON_CANCEL_ACTION_HANDLER); // then give user a chance to modify the default actions currentPage.onEnteringPage(this); // Remove from DecorationPane which has been created by e.g. validation if (currentPage.getParent() != null && currentPage.getParent() instanceof Pane) { Pane parentOfCurrentPage = (Pane) currentPage.getParent(); parentOfCurrentPage.getChildren().remove(currentPage); } // Get current position and size double previousX = dialog.getX(); double previousY = dialog.getY(); double previousWidth = dialog.getWidth(); double previousHeight = dialog.getHeight(); // and then switch to the new pane dialog.setDialogPane(currentPage); // Resize Wizard to new page Window wizard = currentPage.getScene().getWindow(); wizard.sizeToScene(); // Center resized Wizard to previous position if (!Double.isNaN(previousX) && !Double.isNaN(previousY)) { double newWidth = dialog.getWidth(); double newHeight = dialog.getHeight(); int newX = (int) (previousX + (previousWidth / 2.0) - (newWidth / 2.0)); int newY = (int) (previousY + (previousHeight / 2.0) - (newHeight / 2.0)); ObservableList screens = Screen.getScreensForRectangle(previousX, previousY, 1, 1); Screen screen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0); Rectangle2D scrBounds = screen.getBounds(); int minX = (int)Math.round(scrBounds.getMinX()); int maxX = (int)Math.round(scrBounds.getMaxX()); int minY = (int)Math.round(scrBounds.getMinY()); int maxY = (int)Math.round(scrBounds.getMaxY()); if(newX + newWidth > maxX) { newX = maxX - (int)Math.round(newWidth); } if(newY + newHeight > maxY) { newY = maxY - (int)Math.round(newHeight); } if(newX < minX) { newX = minX; } if(newY < minY) { newY = minY; } dialog.setX(newX); dialog.setY(newY); } }); validateActionState(); } private static void addButtonIfMissing(WizardPane page, ButtonType buttonType, EventHandler actionHandler) { List buttons = page.getButtonTypes(); if (! buttons.contains(buttonType)) { buttons.add(buttonType); Button button = (Button)page.lookupButton(buttonType); button.addEventFilter(ActionEvent.ACTION, actionHandler); } } private void validateActionState() { final List currentPaneButtons = dialog.getDialogPane().getButtonTypes(); if (getFlow().canAdvance(currentPage.orElse(null))) { currentPaneButtons.remove(ButtonType.FINISH); } else { currentPaneButtons.remove(BUTTON_NEXT); } validateButton( BUTTON_PREVIOUS, () -> pageHistory.isEmpty()); validateButton( BUTTON_NEXT, () -> invalidProperty.get()); validateButton( ButtonType.FINISH, () -> invalidProperty.get()); } // Functional design allows to delay condition evaluation until it is actually needed private void validateButton( ButtonType buttonType, BooleanSupplier condition) { Button btn = (Button)dialog.getDialogPane().lookupButton(buttonType); if ( btn != null ) { Node focusOwner = (btn.getScene() != null) ? btn.getScene().getFocusOwner() : null; btn.setDisable(condition.getAsBoolean()); if(focusOwner != null) { focusOwner.requestFocus(); } } } private int settingCounter; 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 // 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()); } private boolean checkNode(Node n) { boolean success = readSetting(n); if (success) { // we've added the setting to the settings map and we should stop drilling deeper return true; } else { /** * go into children of this node (if possible) and see if we can get * a value from them (recursively) We use reflection to fix * https://bitbucket.org/controlsfx/controlsfx/issue/412 . */ List children = ImplUtils.getChildren(n, true); // we're doing a depth-first search, where we stop drilling down // once we hit a successful read boolean childSuccess = false; for (Node child : children) { childSuccess |= checkNode(child); } return childSuccess; } } private boolean readSetting(Node n) { if (n == null) { return false; } Object setting = ValueExtractor.getValue(n); if (setting != null) { // save it into the settings map. // if the node has an id set, we will use that as the setting name String settingName = n.getId(); // but if the id is not set, we will use a generic naming scheme if (settingName == null || settingName.isEmpty()) { settingName = "page_" /*+ previousPageIndex*/ + ".setting_" + settingCounter; //$NON-NLS-1$ //$NON-NLS-2$ } getSettings().put(settingName, setting); settingCounter++; } return setting != null; } /************************************************************************** * * Support classes * **************************************************************************/ /** * Represents the page flow of the wizard. It defines only methods required * to move forward in the wizard logic, as backward movement is automatically * handled by wizard itself, using internal page history. */ public interface Flow { /** * Advances the wizard to the next page if possible. * * @param currentPage The current wizard page * @return {@link Optional} value containing the next wizard page. */ Optional advance(WizardPane currentPage); /** * Check if advancing to the next page is possible * * @param currentPage The current wizard page * @return true if it is possible to advance to the next page, false otherwise. */ boolean canAdvance(WizardPane currentPage); } /** * LinearFlow is an implementation of the {@link Wizard.Flow} interface, * designed to support the most common type of wizard flow - namely, a linear * wizard page flow (i.e. through all pages in the order that they are specified). * Therefore, this {@link Flow} implementation simply traverses a collections of * {@link WizardPane WizardPanes}. * *

For example of how to use this API, please refer to the {@link Wizard} * documentation

* * @see Wizard * @see WizardPane */ public static class LinearFlow implements Wizard.Flow { private final List pages; /** * Creates a new LinearFlow instance that will allow for stepping through * the given collection of {@link WizardPane} instances. */ public LinearFlow( Collection pages ) { this.pages = new ArrayList<>(pages); } /** * Creates a new LinearFlow instance that will allow for stepping through * the given varargs array of {@link WizardPane} instances. */ public LinearFlow( WizardPane... pages ) { this( Arrays.asList(pages)); } /** {@inheritDoc} */ @Override public Optional advance(WizardPane currentPage) { int pageIndex = pages.indexOf(currentPage); return Optional.ofNullable( pages.get(++pageIndex) ); } /** {@inheritDoc} */ @Override public boolean canAdvance(WizardPane currentPage) { int pageIndex = pages.indexOf(currentPage); return pages.size()-1 > pageIndex; } } /************************************************************************** * * Methods for the sake of unit tests * **************************************************************************/ /** * @return The {@link Dialog} representing this {@link Wizard}.
* This is actually for {@link Dialog} reading-purposes, e.g. * unit testing the {@link DialogPane} content. */ Dialog getDialog() { return dialog; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy