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 javafx.scene.control;
  26 
  27 import java.lang.ref.WeakReference;
  28 import java.util.ArrayList;
  29 import java.util.Arrays;
  30 import java.util.List;
  31 import java.util.Optional;
  32 
  33 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  34 import javafx.beans.InvalidationListener;
  35 import javafx.beans.NamedArg;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.beans.property.SimpleObjectProperty;
  38 import javafx.collections.ListChangeListener;
  39 import javafx.collections.ObservableList;
  40 
  41 /**
  42  * The Alert class subclasses the {@link Dialog} class, and provides support for a number
  43  * of pre-built dialog types that can be easily shown to users to prompt for a
  44  * response. Therefore, for many users, the Alert class is the most suited class
  45  * for their needs (as opposed to using {@link Dialog} directly). Alternatively,
  46  * users who want to prompt a user for text input or to make a choice from a list
  47  * of options would be better served by using {@link TextInputDialog} and
  48  * {@link ChoiceDialog}, respectively.
  49  *
  50  * <p>When creating an Alert instance, users must pass in an {@link AlertType}
  51  * enumeration value. It is by passing in this value that the Alert instance will
  52  * configure itself appropriately (by setting default values for many of the
  53  * {@link Dialog} properties, including {@link #titleProperty() title},
  54  * {@link #headerTextProperty() header}, and {@link #graphicProperty() graphic},
  55  * as well as the default {@link #getButtonTypes() buttons} that are expected in
  56  * a dialog of the given type.
  57  *
  58  * <p>To instantiate (but not yet show) an Alert, simply use code such as the following:
  59  * {@code Alert alert = new Alert(AlertType.CONFIRMATION, "Are you sure you want to format your system?");}
  60  *
  61  * <p>Once an Alert is instantiated, we must show it. More often than not, alerts
  62  * (and dialogs in general) are shown in a modal and blocking fashion. 'Modal'
  63  * means that the dialog prevents user interaction with the owning application
  64  * whilst it is showing, and 'blocking' means that code execution stops at the
  65  * point in which the dialog is shown. This means that you can show a dialog,
  66  * await the user response, and then continue running the code that directly
  67  * follows the show call, giving developers the ability to immediately deal with
  68  * the user input from the dialog (if relevant).
  69  *
  70  * <p>JavaFX dialogs are modal by default (you can change this via the
  71  * {@link #initModality(javafx.stage.Modality)} API). To specify whether you want
  72  * blocking or non-blocking dialogs, developers simply choose to call
  73  * {@link #showAndWait()} or {@link #show()} (respectively). By default most
  74  * developers should choose to use {@link #showAndWait()}, given the ease of
  75  * coding in these situations. Shown below is three code snippets, showing three
  76  * equally valid ways of showing the Alert dialog that was specified above:
  77  *
  78  * <p><strong>Option 1: The 'traditional' approach</strong>
  79  * <pre>{@code Optional<ButtonType> result = alert.showAndWait();
  80  * if (result.isPresent() && result.get() == ButtonType.OK) {
  81  *     formatSystem();
  82  * }}</pre>
  83  *
  84  * <p><strong>Option 2: The traditional + Optional approach</strong>
  85  * <pre>{@code alert.showAndWait().ifPresent(response -> {
  86  *     if (response == ButtonType.OK) {
  87  *         formatSystem();
  88  *     }
  89  * });}</pre>
  90  *
  91  * <p><strong>Option 3: The fully lambda approach</strong>
  92  * <pre>{@code alert.showAndWait()
  93  *      .filter(response -> response == ButtonType.OK)
  94  *      .ifPresent(response -> formatSystem());
  95  * }</pre>
  96  *
  97  * <p>There is no better or worse option of the three listed above, so developers
  98  * are encouraged to work to their own style preferences. The purpose of showing
  99  * the above is to help introduce developers to the {@link Optional} API, which
 100  * is new in Java 8 and may be foreign to many developers.
 101  *
 102  * @see Dialog
 103  * @see AlertType
 104  * @see TextInputDialog
 105  * @see ChoiceDialog
 106  * @since JavaFX 8u40
 107  */
 108 public class Alert extends Dialog<ButtonType> {
 109 
 110     /**************************************************************************
 111      *
 112      * Static enums
 113      *
 114      **************************************************************************/
 115 
 116     /**
 117      * An enumeration containing the available, pre-built alert types that
 118      * the {@link Alert} class can use to pre-populate various properties.
 119      *
 120      * @since JavaFX 8u40
 121      */
 122     public static enum AlertType {
 123         /**
 124          * The NONE alert type has the effect of not setting any default properties
 125          * in the Alert.
 126          */
 127         NONE,
 128 
 129         /**
 130          * The INFORMATION alert type configures the Alert dialog to appear in a
 131          * way that suggests the content of the dialog is informing the user of
 132          * a piece of information. This includes an 'information' image, an
 133          * appropriate title and header, and just an OK button for the user to
 134          * click on to dismiss the dialog.
 135          */
 136         INFORMATION,
 137 
 138         /**
 139          * The WARNING alert type configures the Alert dialog to appear in a
 140          * way that suggests the content of the dialog is warning the user about
 141          * some fact or action. This includes a 'warning' image, an
 142          * appropriate title and header, and just an OK button for the user to
 143          * click on to dismiss the dialog.
 144          */
 145         WARNING,
 146 
 147         /**
 148          * The CONFIRMATION alert type configures the Alert dialog to appear in a
 149          * way that suggests the content of the dialog is seeking confirmation from
 150          * the user. This includes a 'confirmation' image, an
 151          * appropriate title and header, and both OK and Cancel buttons for the
 152          * user to click on to dismiss the dialog.
 153          */
 154         CONFIRMATION,
 155 
 156         /**
 157          * The ERROR alert type configures the Alert dialog to appear in a
 158          * way that suggests that something has gone wrong. This includes an
 159          * 'error' image, an appropriate title and header, and just an OK button
 160          * for the user to click on to dismiss the dialog.
 161          */
 162         ERROR
 163     }
 164 
 165 
 166 
 167     /**************************************************************************
 168      *
 169      * Fields
 170      *
 171      **************************************************************************/
 172 
 173     private WeakReference<DialogPane> dialogPaneRef;
 174 
 175     private boolean installingDefaults = false;
 176     private boolean hasCustomButtons = false;
 177     private boolean hasCustomTitle = false;
 178     private boolean hasCustomHeaderText = false;
 179 
 180     private final InvalidationListener headerTextListener = o -> {
 181         if (!installingDefaults) hasCustomHeaderText = true;
 182     };
 183 
 184     private final InvalidationListener titleListener = o -> {
 185         if (!installingDefaults) hasCustomTitle = true;
 186     };
 187 
 188     private final ListChangeListener<ButtonType> buttonsListener = change -> {
 189         if (!installingDefaults) hasCustomButtons = true;
 190     };
 191 
 192 
 193 
 194     /**************************************************************************
 195      *
 196      * Constructors
 197      *
 198      **************************************************************************/
 199 
 200     /**
 201      * Creates an alert with the given AlertType (refer to the {@link AlertType}
 202      * documentation for clarification over which one is most appropriate).
 203      *
 204      * <p>By passing in an AlertType, default values for the
 205      * {@link #titleProperty() title}, {@link #headerTextProperty() headerText},
 206      * and {@link #graphicProperty() graphic} properties are set, as well as the
 207      * relevant {@link #getButtonTypes() buttons} being installed. Once the Alert
 208      * is instantiated, developers are able to modify the values of the alert as
 209      * desired.
 210      *
 211      * <p>It is important to note that the one property that does not have a
 212      * default value set, and which therefore the developer must set, is the
 213      * {@link #contentTextProperty() content text} property (or alternatively,
 214      * the developer may call {@code alert.getDialogPane().setContent(Node)} if
 215      * they want a more complex alert). If the contentText (or content) properties
 216      * are not set, there is no useful information presented to end users.
 217      * @param alertType an alert with the given AlertType
 218      */
 219     public Alert(@NamedArg("alertType") AlertType alertType) {
 220         this(alertType, "");
 221     }
 222 
 223     /**
 224      * Creates an alert with the given contentText, ButtonTypes, and AlertType
 225      * (refer to the {@link AlertType} documentation for clarification over which
 226      * one is most appropriate).
 227      *
 228      * <p>By passing in a variable number of ButtonType arguments, the developer
 229      * is directly overriding the default buttons that will be displayed in the
 230      * dialog, replacing the pre-defined buttons with whatever is specified in the
 231      * varargs array.
 232      *
 233      * <p>By passing in an AlertType, default values for the
 234      * {@link #titleProperty() title}, {@link #headerTextProperty() headerText},
 235      * and {@link #graphicProperty() graphic} properties are set. Once the Alert
 236      * is instantiated, developers are able to modify the values of the alert as
 237      * desired.
 238      * @param alertType the alert type
 239      * @param contentText the content text
 240      * @param buttons the button types
 241      */
 242     public Alert(@NamedArg("alertType") AlertType alertType,
 243                  @NamedArg("contentText") String contentText,
 244                  @NamedArg("buttonTypes") ButtonType... buttons) {
 245         super();
 246 
 247         final DialogPane dialogPane = getDialogPane();
 248         dialogPane.setContentText(contentText);
 249         getDialogPane().getStyleClass().add("alert");
 250 
 251         dialogPaneRef = new WeakReference<>(dialogPane);
 252 
 253         hasCustomButtons = buttons != null && buttons.length > 0;
 254         if (hasCustomButtons) {
 255             for (ButtonType btnType : buttons) {
 256                 dialogPane.getButtonTypes().addAll(btnType);
 257             }
 258         }
 259 
 260         setAlertType(alertType);
 261 
 262         // listening to property changes on Dialog and DialogPane
 263         dialogPaneProperty().addListener(o -> updateListeners());
 264         titleProperty().addListener(titleListener);
 265         updateListeners();
 266     }
 267 
 268 
 269 
 270     /**************************************************************************
 271      *
 272      * Properties
 273      *
 274      **************************************************************************/
 275 
 276     /**
 277      * When creating an Alert instance, users must pass in an {@link AlertType}
 278      * enumeration value. It is by passing in this value that the Alert instance will
 279      * configure itself appropriately (by setting default values for many of the
 280      * {@link Dialog} properties, including {@link #titleProperty() title},
 281      * {@link #headerTextProperty() header}, and {@link #graphicProperty() graphic},
 282      * as well as the default {@link #getButtonTypes() buttons} that are expected in
 283      * a dialog of the given type.
 284      */
 285     // --- alertType
 286     private final ObjectProperty<AlertType> alertType = new SimpleObjectProperty<AlertType>(null) {
 287         final String[] styleClasses = new String[] { "information", "warning", "error", "confirmation" };
 288 
 289         @Override
 290         protected void invalidated() {
 291             String newTitle = "";
 292             String newHeader = "";
 293 //            Node newGraphic = null;
 294             String styleClass = "";
 295             ButtonType[] newButtons = new ButtonType[] { ButtonType.OK };
 296             switch (getAlertType()) {
 297                 case NONE: {
 298                     newButtons = new ButtonType[] { };
 299                     break;
 300                 }
 301                 case INFORMATION: {
 302                     newTitle = ControlResources.getString("Dialog.info.title");
 303                     newHeader = ControlResources.getString("Dialog.info.header");
 304                     styleClass = "information";
 305                     break;
 306                 }
 307                 case WARNING: {
 308                     newTitle = ControlResources.getString("Dialog.warning.title");
 309                     newHeader = ControlResources.getString("Dialog.warning.header");
 310                     styleClass = "warning";
 311                     break;
 312                 }
 313                 case ERROR: {
 314                     newTitle = ControlResources.getString("Dialog.error.title");
 315                     newHeader = ControlResources.getString("Dialog.error.header");
 316                     styleClass = "error";
 317                     break;
 318                 }
 319                 case CONFIRMATION: {
 320                     newTitle = ControlResources.getString("Dialog.confirm.title");
 321                     newHeader = ControlResources.getString("Dialog.confirm.header");
 322                     styleClass = "confirmation";
 323                     newButtons = new ButtonType[] { ButtonType.OK, ButtonType.CANCEL };
 324                     break;
 325                 }
 326             }
 327 
 328             installingDefaults = true;
 329             if (!hasCustomTitle) setTitle(newTitle);
 330             if (!hasCustomHeaderText) setHeaderText(newHeader);
 331             if (!hasCustomButtons) getButtonTypes().setAll(newButtons);
 332 
 333             // update the style class based on the alert type. We use this to
 334             // specify the default graphic to use (i.e. via CSS).
 335             DialogPane dialogPane = getDialogPane();
 336             if (dialogPane != null) {
 337                 List<String> toRemove = new ArrayList<>(Arrays.asList(styleClasses));
 338                 toRemove.remove(styleClass);
 339                 dialogPane.getStyleClass().removeAll(toRemove);
 340                 if (! dialogPane.getStyleClass().contains(styleClass)) {
 341                     dialogPane.getStyleClass().add(styleClass);
 342                 }
 343             }
 344 
 345             installingDefaults = false;
 346         }
 347     };
 348 
 349     public final AlertType getAlertType() {
 350         return alertType.get();
 351     }
 352 
 353     public final void setAlertType(AlertType alertType) {
 354         this.alertType.setValue(alertType);
 355     }
 356 
 357     public final ObjectProperty<AlertType> alertTypeProperty() {
 358         return alertType;
 359     }
 360 
 361 
 362     /**
 363      * Returns an {@link ObservableList} of all {@link ButtonType} instances that
 364      * are currently set inside this Alert instance. A ButtonType may either be one
 365      * of the pre-defined types (e.g. {@link ButtonType#OK}), or it may be a
 366      * custom type (created via the {@link ButtonType#ButtonType(String)} or
 367      * {@link ButtonType#ButtonType(String, javafx.scene.control.ButtonBar.ButtonData)}
 368      * constructors.
 369      *
 370      * <p>Readers should refer to the {@link ButtonType} class documentation for more details,
 371      * but at a high level, each ButtonType instance is converted to
 372      * a Node (although most commonly a {@link Button}) via the (overridable)
 373      * {@link DialogPane#createButton(ButtonType)} method on {@link DialogPane}.
 374      * @return an {@link ObservableList} of all {@link ButtonType} instances that
 375      * are currently set inside this Alert instance
 376      */
 377     // --- buttonTypes
 378     public final ObservableList<ButtonType> getButtonTypes() {
 379         return getDialogPane().getButtonTypes();
 380     }
 381 
 382 
 383 
 384     /**************************************************************************
 385      *
 386      * Private Implementation
 387      *
 388      **************************************************************************/
 389 
 390     private void updateListeners() {
 391         DialogPane oldPane = dialogPaneRef.get();
 392 
 393         if (oldPane != null) {
 394             oldPane.headerTextProperty().removeListener(headerTextListener);
 395             oldPane.getButtonTypes().removeListener(buttonsListener);
 396         }
 397 
 398         // listen to changes to properties that would be changed by alertType being
 399         // changed, so that we only change values that are still at their default
 400         // value (i.e. the user hasn't changed them, so we are free to set them
 401         // to a new default value when the alertType changes).
 402 
 403         DialogPane newPane = getDialogPane();
 404         if (newPane != null) {
 405             newPane.headerTextProperty().addListener(headerTextListener);
 406             newPane.getButtonTypes().addListener(buttonsListener);
 407         }
 408 
 409         dialogPaneRef = new WeakReference<DialogPane>(newPane);
 410     }
 411 }