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 }