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 }