/* * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package hello.dialog.wizard; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.Stack; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; import javafx.scene.control.DialogPane; import javafx.scene.control.ButtonBar.ButtonData; import javafx.scene.image.Image; import javafx.scene.image.ImageView; public class Wizard { /************************************************************************** * * Static fields * **************************************************************************/ /************************************************************************** * * Private fields * **************************************************************************/ private Dialog dialog; private final ObservableMap settings = FXCollections.observableHashMap(); private final Stack pageHistory = new Stack<>(); private Optional currentPage = Optional.empty(); // private final ValidationSupport validationSupport = new ValidationSupport(); // private final ButtonType BUTTON_PREVIOUS = new ButtonType("Previous", ButtonData.BACK_PREVIOUS); private final EventHandler BUTTON_PREVIOUS_ACTION_HANDLER = actionEvent -> { actionEvent.consume(); currentPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.pop() ); updatePage(dialog,false); }; private final ButtonType BUTTON_NEXT = new ButtonType("Next", ButtonData.NEXT_FORWARD); private final EventHandler BUTTON_NEXT_ACTION_HANDLER = actionEvent -> { actionEvent.consume(); currentPage.ifPresent(page->pageHistory.push(page)); currentPage = getFlow().advance(currentPage.orElse(null)); updatePage(dialog,true); }; /************************************************************************** * * Constructors * **************************************************************************/ /** * */ public Wizard() { this(null); } /** * * @param owner */ private Wizard(Object owner) { this(owner, ""); } /** * * @param owner * @param title */ private Wizard(Object owner, String title) { // this.owner = owner; // this.title = title; // validationSupport.validationResultProperty().addListener( (o, ov, nv) -> validateActionState()); dialog = new Dialog(); dialog.setTitle(title); // hello.dialog.initOwner(owner); // TODO add initOwner API } /************************************************************************** * * Public API * **************************************************************************/ public final void show() { dialog.show(); } public final Optional showAndWait() { return dialog.showAndWait(); } // --- settings public final ObservableMap getSettings() { return settings; } /************************************************************************** * * Properties * **************************************************************************/ // --- flow private ObjectProperty flow = new SimpleObjectProperty(new LinearWizardFlow()) { protected void invalidated() { updatePage(dialog,false); } public void set(Flow flow) { super.set(flow); pageHistory.clear(); if ( flow != null ) { currentPage = flow.advance(currentPage.orElse(null)); updatePage(dialog,true); } }; }; public final ObjectProperty flowProperty() { return flow; } public final Flow getFlow() { return flow.get(); } 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. * * @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 hello.dialog.wizard.Wizard#getUserData()}. * * @param value The value to be stored - this can later be retrieved by calling * {@link hello.dialog.wizard.Wizard#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 hello.dialog.wizard.Wizard#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); } // public ValidationSupport getValidationSupport() { // return validationSupport; // } /************************************************************************** * * Private implementation * **************************************************************************/ private void updatePage(Dialog dialog, boolean advancing) { Flow flow = getFlow(); if (flow == null) { return; } Optional prevPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.peek()); 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 if (advancing) { 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.ifPresent(currentPage -> { // put in default actions List buttons = currentPage.getButtonTypes(); if (! buttons.contains(BUTTON_PREVIOUS)) { buttons.add(BUTTON_PREVIOUS); Button button = (Button)currentPage.lookupButton(BUTTON_PREVIOUS); button.addEventFilter(ActionEvent.ACTION, BUTTON_PREVIOUS_ACTION_HANDLER); } if (! buttons.contains(BUTTON_NEXT)) { buttons.add(BUTTON_NEXT); Button button = (Button)currentPage.lookupButton(BUTTON_NEXT); button.addEventFilter(ActionEvent.ACTION, BUTTON_NEXT_ACTION_HANDLER); } if (! buttons.contains(ButtonType.FINISH)) buttons.add(ButtonType.FINISH); if (! buttons.contains(ButtonType.CANCEL)) buttons.add(ButtonType.CANCEL); // then give user a chance to modify the default actions currentPage.onEnteringPage(this); // and then switch to the new pane dialog.setDialogPane(currentPage); }); validateActionState(); } private void validateActionState() { final List currentPaneButtons = dialog.getDialogPane().getButtonTypes(); // TODO can't set a DialogButton to be disabled at present // BUTTON_PREVIOUS.setDisabled(pageHistory.isEmpty()); // Note that we put the 'next' and 'finish' actions at the beginning of // the actions list, so that it takes precedence as the default button, // over, say, cancel. We will probably want to handle this better in the // future... if (!getFlow().canAdvance(currentPage.orElse(null))) { currentPaneButtons.remove(BUTTON_NEXT); // currentPaneActions.add(0, ACTION_FINISH); // ACTION_FINISH.setDisabled( validationSupport.isInvalid()); } else { if (currentPaneButtons.contains(BUTTON_NEXT)) { currentPaneButtons.remove(BUTTON_NEXT); currentPaneButtons.add(0, BUTTON_NEXT); Button button = (Button)dialog.getDialogPane().lookupButton(BUTTON_NEXT); button.addEventFilter(ActionEvent.ACTION, BUTTON_NEXT_ACTION_HANDLER); } currentPaneButtons.remove(ButtonType.FINISH); // ACTION_NEXT.setDisabled( validationSupport.isInvalid()); } } 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) List children = ImplUtils.getChildren(n); // 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; } getSettings().put(settingName, setting); settingCounter++; } return setting != null; } /************************************************************************** * * Support classes * **************************************************************************/ /** * */ // TODO this should just contain a ControlsFX Form, but for now it is hand-coded public static class WizardPane extends DialogPane { public WizardPane() { // TODO extract to CSS setGraphic(new ImageView(new Image(getClass().getResource("/hello/dialog/dialog-confirm.png").toExternalForm()))); } // TODO we want to change this to an event-based API eventually public void onEnteringPage(Wizard wizard) { } // TODO same here - replace with events public void onExitingPage(Wizard wizard) { } } public interface Flow { Optional advance(WizardPane currentPage); boolean canAdvance(WizardPane currentPage); } }