1 /* 2 * Copyright (c) 2012, 2014, Oracle and/or its affiliates. 3 * All rights reserved. Use is subject to license terms. 4 * 5 * This file is available and licensed under the following license: 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * - Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * - Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the distribution. 16 * - Neither the name of Oracle Corporation nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog; 33 34 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N; 35 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform; 36 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlWindowController; 37 import java.io.IOException; 38 import java.net.URL; 39 import java.util.ResourceBundle; 40 import javafx.event.ActionEvent; 41 import javafx.event.EventHandler; 42 import javafx.fxml.FXML; 43 import javafx.fxml.FXMLLoader; 44 import javafx.scene.Parent; 45 import javafx.scene.control.Button; 46 import javafx.scene.image.Image; 47 import javafx.scene.image.ImageView; 48 import javafx.scene.layout.Pane; 49 import javafx.scene.layout.StackPane; 50 import javafx.stage.Modality; 51 import javafx.stage.Window; 52 import javafx.stage.WindowEvent; 53 54 /** 55 * 56 * 57 */ 58 public abstract class AbstractModalDialog extends AbstractFxmlWindowController { 59 60 public enum ButtonID { OK, CANCEL, ACTION } 61 62 private final Window owner; 63 private final URL contentFxmlURL; 64 private final ResourceBundle contentResources; 65 private Parent contentRoot; 66 private ButtonID clickedButtonID; 67 private boolean showDefaultButton; 68 private ButtonID defaultButtonID = ButtonID.OK; 69 private boolean focusTraversableButtons; 70 71 /* 72 * The following members should be considered as 'private'. 73 * They are 'protected' only to please the FXML loader. 74 */ 75 @FXML protected StackPane contentPane; 76 @FXML protected Button okButton; 77 @FXML protected Button cancelButton; 78 @FXML protected Button actionButton; 79 @FXML protected Pane okParent; 80 @FXML protected Pane actionParent; 81 @FXML protected ImageView imageView; 82 @FXML protected Pane imageViewParent; 83 84 /* 85 * Public 86 */ 87 88 public AbstractModalDialog(URL contentFxmlURL, ResourceBundle contentResources, Window owner) { 89 super(getContainerFxmlURL(), I18N.getBundle()); 90 this.owner = owner; 91 this.contentFxmlURL = contentFxmlURL; 92 this.contentResources = contentResources; 93 assert contentFxmlURL != null; 94 } 95 96 public Parent getContentRoot() { 97 98 if (contentRoot == null) { 99 final FXMLLoader loader = new FXMLLoader(); 100 101 loader.setController(this); 102 loader.setLocation(contentFxmlURL); 103 loader.setResources(contentResources); 104 try { 105 contentRoot = (Parent)loader.load(); 106 controllerDidLoadContentFxml(); 107 } catch (IOException x) { 108 contentRoot = null; 109 throw new RuntimeException("Failed to load " + contentFxmlURL.getFile(), x); //NOI18N 110 } 111 } 112 113 return contentRoot; 114 } 115 116 public final ButtonID showAndWait() { 117 // center(); 118 clickedButtonID = ButtonID.CANCEL; 119 getStage().showAndWait(); 120 return clickedButtonID; 121 } 122 123 public String getTitle() { 124 return getStage().getTitle(); 125 } 126 127 public void setTitle(String title) { 128 getStage().setTitle(title); 129 } 130 131 public String getOKButtonTitle() { 132 return getOKButton().getText(); 133 } 134 135 public void setOKButtonTitle(String title) { 136 getOKButton().setText(title); 137 } 138 139 public String getCancelButtonTitle() { 140 return getCancelButton().getText(); 141 } 142 143 public void setCancelButtonTitle(String title) { 144 getCancelButton().setText(title); 145 } 146 147 public String getActionButtonTitle() { 148 return getActionButton().getText(); 149 } 150 151 public void setActionButtonTitle(String title) { 152 getActionButton().setText(title); 153 } 154 155 public boolean isOKButtonVisible() { 156 return getOKButton().getParent() != null; 157 } 158 159 public void setOKButtonVisible(boolean visible) { 160 if (visible != isOKButtonVisible()) { 161 if (visible) { 162 assert getOKButton().getParent() == null; 163 getOKParent().getChildren().add(getOKButton()); 164 } else { 165 assert getOKButton().getParent() == getOKParent(); 166 getOKParent().getChildren().remove(getOKButton()); 167 } 168 } 169 } 170 171 public boolean isActionButtonVisible() { 172 return getActionButton().getParent() != null; 173 } 174 175 public void setActionButtonVisible(boolean visible) { 176 if (visible != isActionButtonVisible()) { 177 if (visible) { 178 assert getActionButton().getParent() == null; 179 getActionParent().getChildren().add(getActionButton()); 180 } else { 181 assert getActionButton().getParent() == getActionParent(); 182 getActionParent().getChildren().remove(getActionButton()); 183 } 184 } 185 } 186 187 public void setOKButtonDisable(boolean disable) { 188 getOKButton().setDisable(disable); 189 } 190 191 public void setActionButtonDisable(boolean disable) { 192 getActionButton().setDisable(disable); 193 } 194 195 public void setShowDefaultButton(boolean show) { 196 showDefaultButton = show; 197 updateButtonState(); 198 } 199 200 public void setDefaultButtonID(ButtonID buttonID) { 201 defaultButtonID = buttonID; 202 updateButtonState(); 203 } 204 205 public boolean isImageViewVisible() { 206 return getImageView().getParent() != null; 207 } 208 209 public void setImageViewVisible(boolean visible) { 210 if (visible != isImageViewVisible()) { 211 if (visible) { 212 assert getImageView().getParent() == null; 213 imageViewParent.getChildren().add(getImageView()); 214 } else { 215 assert getImageView().getParent() == imageViewParent; 216 imageViewParent.getChildren().remove(getImageView()); 217 } 218 } 219 } 220 221 public Image getImageViewImage() { 222 return getImageView().getImage(); 223 } 224 225 public void setImageViewImage(Image image) { 226 getImageView().setImage(image); 227 } 228 229 // On Mac the FXML defines the 3 buttons as non focus traversable. 230 // However for complex dialogs such a Preferences, Code Skeleton and 231 // Preview Background Color we'd better have them focus traversable hence 232 // this method. 233 public void setButtonsFocusTraversable() { 234 if (EditorPlatform.IS_MAC) { 235 getOKButton().setFocusTraversable(true); 236 getCancelButton().setFocusTraversable(true); 237 getActionButton().setFocusTraversable(true); 238 focusTraversableButtons = true; 239 } 240 } 241 242 /* 243 * To be subclassed 244 */ 245 246 protected abstract void controllerDidLoadContentFxml(); 247 248 /* 249 * To be subclassed #2 250 */ 251 @FXML 252 protected abstract void okButtonPressed(ActionEvent e); 253 254 @FXML 255 protected abstract void cancelButtonPressed(ActionEvent e); 256 257 @FXML 258 protected abstract void actionButtonPressed(ActionEvent e); 259 260 261 /* 262 * AbstractWindowController 263 */ 264 265 @Override 266 protected void controllerDidCreateStage() { 267 if (this.owner == null) { 268 // Dialog will be appliation modal 269 getStage().initModality(Modality.APPLICATION_MODAL); 270 } else { 271 // Dialog will be window modal 272 getStage().initOwner(this.owner); 273 getStage().initModality(Modality.WINDOW_MODAL); 274 } 275 } 276 277 /* 278 * AbstractFxmlWindowController 279 */ 280 281 @Override 282 protected void controllerDidLoadFxml() { 283 assert contentPane != null; 284 assert okButton != null; 285 assert cancelButton != null; 286 assert actionButton != null; 287 assert imageView != null; 288 assert okParent != null; 289 assert actionParent != null; 290 assert imageViewParent != null; 291 assert okButton.getParent() == okParent; 292 assert actionButton.getParent() == actionParent; 293 assert imageView.getParent() == imageViewParent; 294 295 final EventHandler<ActionEvent> callUpdateButtonID = e -> updateButtonID(e); 296 okButton.addEventHandler(ActionEvent.ACTION, callUpdateButtonID); 297 cancelButton.addEventHandler(ActionEvent.ACTION, callUpdateButtonID); 298 actionButton.addEventHandler(ActionEvent.ACTION, callUpdateButtonID); 299 300 contentPane.getChildren().add(getContentRoot()); 301 302 // By default, action button and image view are not visible 303 setActionButtonVisible(false); 304 setImageViewVisible(false); 305 306 // Setup default state and focus 307 updateButtonState(); 308 309 // Size everything 310 getStage().sizeToScene(); 311 } 312 313 @Override 314 public void onCloseRequest(WindowEvent event) { 315 // Closing the window is equivalent to clicking the Cancel button 316 cancelButtonPressed(null); 317 } 318 319 320 /* 321 * Private 322 */ 323 private static URL getContainerFxmlURL() { 324 final String fxmlName; 325 326 if (EditorPlatform.IS_WINDOWS) { 327 fxmlName = "AbstractModalDialogW.fxml"; //NOI18N 328 } else { 329 fxmlName = "AbstractModalDialogM.fxml"; //NOI18N 330 } 331 332 return AbstractModalDialog.class.getResource(fxmlName); 333 } 334 335 private Button getOKButton() { 336 getRoot(); // Force fxml loading 337 return okButton; 338 } 339 340 private Button getCancelButton() { 341 getRoot(); // Force fxml loading 342 return cancelButton; 343 } 344 345 private Button getActionButton() { 346 getRoot(); // Force fxml loading 347 return actionButton; 348 } 349 350 private Pane getOKParent() { 351 getRoot(); // Force fxml loading 352 return okParent; 353 } 354 355 private Pane getActionParent() { 356 getRoot(); // Force fxml loading 357 return actionParent; 358 } 359 360 private ImageView getImageView() { 361 getRoot(); // Force fxml loading 362 return imageView; 363 } 364 365 private void updateButtonID(ActionEvent t) { 366 assert t != null; 367 368 final Object source = t.getSource(); 369 if (source == getCancelButton()) { 370 clickedButtonID = AbstractModalDialog.ButtonID.CANCEL; 371 } else if (source == getOKButton()) { 372 clickedButtonID = AbstractModalDialog.ButtonID.OK; 373 } else if (source == getActionButton()) { 374 clickedButtonID = AbstractModalDialog.ButtonID.ACTION; 375 } else { 376 throw new IllegalArgumentException("Bug"); //NOI18N 377 } 378 } 379 380 private void updateButtonState() { 381 getOKButton().setDefaultButton(false); 382 getCancelButton().setDefaultButton(false); 383 getActionButton().setDefaultButton(false); 384 385 // To stick to OS specific "habits" we set a default button on Mac as on 386 // Win and Linux we use focus to mark a button as the default one (then 387 // you can tab navigate from a button to another, something which is 388 // disabled on Mac. 389 // However on Mac and for complex dialogs (Preferences, Code Skeleton, 390 // Background Color) we apply the Win/Linux scheme: buttons are focus 391 // traversable and there's no default one. The user needs to press Space 392 // to take action with the focused button. We take this approach because 393 // complex dialogs contain editable field that take focus and that 394 // interferes with a button set as default one. 395 // See DTL-5333. 396 if (showDefaultButton) { 397 switch(defaultButtonID) { 398 case OK: 399 if (EditorPlatform.IS_MAC && ! focusTraversableButtons) { 400 getOKButton().setDefaultButton(true); 401 } else { 402 getOKButton().requestFocus(); 403 } 404 break; 405 case CANCEL: 406 if (EditorPlatform.IS_MAC && ! focusTraversableButtons) { 407 getCancelButton().setDefaultButton(true); 408 } else { 409 getCancelButton().requestFocus(); 410 } 411 break; 412 case ACTION: 413 if (EditorPlatform.IS_MAC && ! focusTraversableButtons) { 414 getActionButton().setDefaultButton(true); 415 } else { 416 getActionButton().requestFocus(); 417 } 418 break; 419 } 420 } 421 } 422 423 }