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 }