1 /*
   2  * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package hello.dialog.wizard;
  26 
  27 import java.util.HashMap;
  28 import java.util.List;
  29 import java.util.Optional;
  30 import java.util.Stack;
  31 
  32 import javafx.beans.property.ObjectProperty;
  33 import javafx.beans.property.SimpleObjectProperty;
  34 import javafx.collections.FXCollections;
  35 import javafx.collections.ObservableMap;
  36 import javafx.event.ActionEvent;
  37 import javafx.event.EventHandler;
  38 import javafx.scene.Node;
  39 import javafx.scene.control.Button;
  40 import javafx.scene.control.ButtonType;
  41 import javafx.scene.control.Dialog;
  42 import javafx.scene.control.DialogPane;
  43 import javafx.scene.control.ButtonBar.ButtonData;
  44 import javafx.scene.image.Image;
  45 import javafx.scene.image.ImageView;
  46 
  47 public class Wizard {
  48 
  49 
  50     /**************************************************************************
  51      *
  52      * Static fields
  53      *
  54      **************************************************************************/
  55 
  56 
  57 
  58     /**************************************************************************
  59      *
  60      * Private fields
  61      *
  62      **************************************************************************/
  63 
  64     private Dialog<ButtonType> dialog;
  65 
  66     private final ObservableMap<String, Object> settings = FXCollections.observableHashMap();
  67 
  68     private final Stack<WizardPane> pageHistory = new Stack<>();
  69 
  70     private Optional<WizardPane> currentPage = Optional.empty();
  71 
  72 //    private final ValidationSupport validationSupport = new ValidationSupport();
  73 
  74 //
  75     private final ButtonType BUTTON_PREVIOUS = new ButtonType("Previous", ButtonData.BACK_PREVIOUS);
  76     private final EventHandler<ActionEvent> BUTTON_PREVIOUS_ACTION_HANDLER = actionEvent -> {
  77         actionEvent.consume();
  78         currentPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.pop() );
  79         updatePage(dialog,false);
  80     };
  81 
  82     private final ButtonType BUTTON_NEXT = new ButtonType("Next", ButtonData.NEXT_FORWARD);
  83     private final EventHandler<ActionEvent> BUTTON_NEXT_ACTION_HANDLER = actionEvent -> {
  84         actionEvent.consume();
  85         currentPage.ifPresent(page->pageHistory.push(page));
  86         currentPage = getFlow().advance(currentPage.orElse(null));
  87         updatePage(dialog,true);
  88     };
  89 
  90 
  91 
  92     /**************************************************************************
  93      *
  94      * Constructors
  95      *
  96      **************************************************************************/
  97 
  98     /**
  99      *
 100      */
 101     public Wizard() {
 102         this(null);
 103     }
 104 
 105     /**
 106      *
 107      * @param owner
 108      */
 109     private Wizard(Object owner) {
 110         this(owner, "");
 111     }
 112 
 113     /**
 114      *
 115      * @param owner
 116      * @param title
 117      */
 118     private Wizard(Object owner, String title) {
 119 //        this.owner = owner;
 120 //        this.title = title;
 121 
 122 //        validationSupport.validationResultProperty().addListener( (o, ov, nv) -> validateActionState());
 123 
 124         dialog = new Dialog<ButtonType>();
 125         dialog.setTitle(title);
 126 //        hello.dialog.initOwner(owner); // TODO add initOwner API
 127 
 128     }
 129 
 130 
 131 
 132     /**************************************************************************
 133      *
 134      * Public API
 135      *
 136      **************************************************************************/
 137 
 138     public final void show() {
 139         dialog.show();
 140     }
 141 
 142     public final Optional<ButtonType> showAndWait() {
 143         return dialog.showAndWait();
 144     }
 145 
 146     // --- settings
 147     public final ObservableMap<String, Object> getSettings() {
 148         return settings;
 149     }
 150 
 151 
 152 
 153     /**************************************************************************
 154      *
 155      * Properties
 156      *
 157      **************************************************************************/
 158 
 159     // --- flow
 160     private ObjectProperty<Flow> flow = new SimpleObjectProperty<Flow>(new LinearWizardFlow()) {
 161         protected void invalidated() {
 162             updatePage(dialog,false);
 163         }
 164 
 165         public void set(Flow flow) {
 166             super.set(flow);
 167             pageHistory.clear();
 168             if ( flow != null ) {
 169                 currentPage = flow.advance(currentPage.orElse(null));
 170                 updatePage(dialog,true);
 171             }
 172         };
 173     };
 174 
 175     public final ObjectProperty<Flow> flowProperty() {
 176         return flow;
 177     }
 178 
 179     public final Flow getFlow() {
 180         return flow.get();
 181     }
 182 
 183     public final void setFlow(Flow flow) {
 184         this.flow.set(flow);
 185     }
 186 
 187 
 188     // --- Properties
 189     private static final Object USER_DATA_KEY = new Object();
 190 
 191     // A map containing a set of properties for this Wizard
 192     private ObservableMap<Object, Object> properties;
 193 
 194     /**
 195       * Returns an observable map of properties on this Wizard for use primarily
 196       * by application developers.
 197       *
 198       * @return an observable map of properties on this Wizard for use primarily
 199       * by application developers
 200      */
 201      public final ObservableMap<Object, Object> getProperties() {
 202         if (properties == null) {
 203             properties = FXCollections.observableMap(new HashMap<Object, Object>());
 204         }
 205         return properties;
 206     }
 207 
 208     /**
 209      * Tests if this Wizard has properties.
 210      * @return true if this Wizard has properties.
 211      */
 212      public boolean hasProperties() {
 213         return properties != null && !properties.isEmpty();
 214     }
 215 
 216 
 217     // --- UserData
 218     /**
 219      * Convenience method for setting a single Object property that can be
 220      * retrieved at a later date. This is functionally equivalent to calling
 221      * the getProperties().put(Object key, Object value) method. This can later
 222      * be retrieved by calling {@link hello.dialog.wizard.Wizard#getUserData()}.
 223      *
 224      * @param value The value to be stored - this can later be retrieved by calling
 225      *          {@link hello.dialog.wizard.Wizard#getUserData()}.
 226      */
 227     public void setUserData(Object value) {
 228         getProperties().put(USER_DATA_KEY, value);
 229     }
 230 
 231     /**
 232      * Returns a previously set Object property, or null if no such property
 233      * has been set using the {@link hello.dialog.wizard.Wizard#setUserData(Object)} method.
 234      *
 235      * @return The Object that was previously set, or null if no property
 236      *          has been set or if null was set.
 237      */
 238     public Object getUserData() {
 239         return getProperties().get(USER_DATA_KEY);
 240     }
 241 
 242 
 243 //    public ValidationSupport getValidationSupport() {
 244 //      return validationSupport;
 245 //  }
 246 
 247 
 248     /**************************************************************************
 249      *
 250      * Private implementation
 251      *
 252      **************************************************************************/
 253 
 254     private void updatePage(Dialog<ButtonType> dialog, boolean advancing) {
 255         Flow flow = getFlow();
 256         if (flow == null) {
 257             return;
 258         }
 259 
 260         Optional<WizardPane> prevPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.peek());
 261         prevPage.ifPresent( page -> {
 262             // if we are going forward in the wizard, we read in the settings
 263             // from the page and store them in the settings map.
 264             // If we are going backwards, we do nothing
 265             if (advancing) {
 266                 readSettings(page);
 267             }
 268 
 269             // give the previous wizard page a chance to update the pages list
 270             // based on the settings it has received
 271             page.onExitingPage(this);
 272         });
 273 
 274         currentPage.ifPresent(currentPage -> {
 275             // put in default actions
 276             List<ButtonType> buttons = currentPage.getButtonTypes();
 277             if (! buttons.contains(BUTTON_PREVIOUS)) {
 278                 buttons.add(BUTTON_PREVIOUS);
 279                 Button button = (Button)currentPage.lookupButton(BUTTON_PREVIOUS);
 280                 button.addEventFilter(ActionEvent.ACTION, BUTTON_PREVIOUS_ACTION_HANDLER);
 281             }
 282             if (! buttons.contains(BUTTON_NEXT)) {
 283                 buttons.add(BUTTON_NEXT);
 284                 Button button = (Button)currentPage.lookupButton(BUTTON_NEXT);
 285                 button.addEventFilter(ActionEvent.ACTION, BUTTON_NEXT_ACTION_HANDLER);
 286             }
 287             if (! buttons.contains(ButtonType.FINISH)) buttons.add(ButtonType.FINISH);
 288             if (! buttons.contains(ButtonType.CANCEL)) buttons.add(ButtonType.CANCEL);
 289 
 290             // then give user a chance to modify the default actions
 291             currentPage.onEnteringPage(this);
 292 
 293             // and then switch to the new pane
 294             dialog.setDialogPane(currentPage);
 295         });
 296 
 297         validateActionState();
 298     }
 299 
 300     private void validateActionState() {
 301         final List<ButtonType> currentPaneButtons = dialog.getDialogPane().getButtonTypes();
 302 
 303         // TODO can't set a DialogButton to be disabled at present
 304 //        BUTTON_PREVIOUS.setDisabled(pageHistory.isEmpty());
 305 
 306         // Note that we put the 'next' and 'finish' actions at the beginning of
 307         // the actions list, so that it takes precedence as the default button,
 308         // over, say, cancel. We will probably want to handle this better in the
 309         // future...
 310 
 311         if (!getFlow().canAdvance(currentPage.orElse(null))) {
 312             currentPaneButtons.remove(BUTTON_NEXT);
 313 
 314 //            currentPaneActions.add(0, ACTION_FINISH);
 315 //            ACTION_FINISH.setDisabled( validationSupport.isInvalid());
 316         } else {
 317             if (currentPaneButtons.contains(BUTTON_NEXT)) {
 318                 currentPaneButtons.remove(BUTTON_NEXT);
 319                 currentPaneButtons.add(0, BUTTON_NEXT);
 320                 Button button = (Button)dialog.getDialogPane().lookupButton(BUTTON_NEXT);
 321                 button.addEventFilter(ActionEvent.ACTION, BUTTON_NEXT_ACTION_HANDLER);
 322             }
 323             currentPaneButtons.remove(ButtonType.FINISH);
 324 //            ACTION_NEXT.setDisabled( validationSupport.isInvalid());
 325         }
 326     }
 327 
 328     private int settingCounter;
 329     private void readSettings(WizardPane page) {
 330         // for now we cannot know the structure of the page, so we just drill down
 331         // through the entire scenegraph (from page.content down) until we get
 332         // to the leaf nodes. We stop only if we find a node that is a
 333         // ValueContainer (either by implementing the interface), or being
 334         // listed in the internal valueContainers map.
 335 
 336         settingCounter = 0;
 337         checkNode(page.getContent());
 338     }
 339 
 340     private boolean checkNode(Node n) {
 341         boolean success = readSetting(n);
 342 
 343         if (success) {
 344             // we've added the setting to the settings map and we should stop drilling deeper
 345             return true;
 346         } else {
 347             // go into children of this node (if possible) and see if we can get
 348             // a value from them (recursively)
 349             List<Node> children = ImplUtils.getChildren(n);
 350 
 351             // we're doing a depth-first search, where we stop drilling down
 352             // once we hit a successful read
 353             boolean childSuccess = false;
 354             for (Node child : children) {
 355                 childSuccess |= checkNode(child);
 356             }
 357             return childSuccess;
 358         }
 359     }
 360 
 361     private boolean readSetting(Node n) {
 362         if (n == null) {
 363             return false;
 364         }
 365 
 366         Object setting = ValueExtractor.getValue(n);
 367 
 368         if (setting != null) {
 369             // save it into the settings map.
 370             // if the node has an id set, we will use that as the setting name
 371             String settingName = n.getId();
 372 
 373             // but if the id is not set, we will use a generic naming scheme
 374             if (settingName == null || settingName.isEmpty()) {
 375                 settingName = "page_" /*+ previousPageIndex*/ + ".setting_" + settingCounter;
 376             }
 377 
 378             getSettings().put(settingName, setting);
 379 
 380             settingCounter++;
 381         }
 382 
 383         return setting != null;
 384     }
 385 
 386 
 387 
 388     /**************************************************************************
 389      *
 390      * Support classes
 391      *
 392      **************************************************************************/
 393 
 394     /**
 395      *
 396      */
 397     // TODO this should just contain a ControlsFX Form, but for now it is hand-coded
 398     public static class WizardPane extends DialogPane {
 399 
 400         public WizardPane() {
 401             // TODO extract to CSS
 402             setGraphic(new ImageView(new Image(getClass().getResource("/hello/dialog/dialog-confirm.png").toExternalForm())));
 403         }
 404 
 405         // TODO we want to change this to an event-based API eventually
 406         public void onEnteringPage(Wizard wizard) {
 407 
 408         }
 409 
 410         // TODO same here - replace with events
 411         public void onExitingPage(Wizard wizard) {
 412 
 413         }
 414     }
 415 
 416     public interface Flow {
 417         Optional<WizardPane> advance(WizardPane currentPage);
 418         boolean canAdvance(WizardPane currentPage);
 419     }
 420 
 421 }