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