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 }