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.Collections; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.WeakHashMap; 33 34 import com.sun.javafx.scene.control.skin.Utils; 35 import com.sun.javafx.scene.control.skin.resources.ControlResources; 36 import javafx.beans.DefaultProperty; 37 import javafx.beans.InvalidationListener; 38 import javafx.beans.property.BooleanProperty; 39 import javafx.beans.property.ObjectProperty; 40 import javafx.beans.property.SimpleBooleanProperty; 41 import javafx.beans.property.SimpleObjectProperty; 42 import javafx.beans.property.SimpleStringProperty; 43 import javafx.beans.property.StringProperty; 44 import javafx.beans.value.WritableValue; 45 import javafx.collections.FXCollections; 46 import javafx.collections.ListChangeListener; 47 import javafx.collections.ObservableList; 48 import javafx.css.CssMetaData; 49 import javafx.css.StyleOrigin; 50 import javafx.css.Styleable; 51 import javafx.css.StyleableObjectProperty; 52 import javafx.css.StyleableProperty; 53 import javafx.css.StyleableStringProperty; 54 import javafx.event.ActionEvent; 55 import javafx.geometry.Pos; 56 import javafx.scene.Node; 57 import javafx.scene.control.ButtonBar.ButtonData; 58 import javafx.scene.image.Image; 59 import javafx.scene.image.ImageView; 60 import javafx.scene.layout.ColumnConstraints; 61 import javafx.scene.layout.GridPane; 62 import javafx.scene.layout.Pane; 63 import javafx.scene.layout.Priority; 64 import javafx.scene.layout.Region; 65 import javafx.scene.layout.StackPane; 66 67 import com.sun.javafx.css.StyleManager; 68 import javafx.css.converter.StringConverter; 69 70 /** 71 * DialogPane should be considered to be the root node displayed within a 72 * {@link Dialog} instance. In this role, the DialogPane is responsible for the 73 * placement of {@link #headerProperty() headers}, {@link #graphicProperty() graphics}, 74 * {@link #contentProperty() content}, and {@link #getButtonTypes() buttons}. 75 * The default implementation of DialogPane (that is, the DialogPane class itself) 76 * handles the layout via the normal {@link #layoutChildren()} method. This 77 * method may be overridden by subclasses wishing to handle the layout in an 78 * alternative fashion). 79 * 80 * <p>In addition to the {@link #headerProperty() header} and 81 * {@link #contentProperty() content} properties, there exists 82 * {@link #headerTextProperty() header text} and 83 * {@link #contentTextProperty() content text} properties. The way the *Text 84 * properties work is that they are a lower precedence compared to the Node 85 * properties, but they are far more convenient for developers in the common case, 86 * as it is likely the case that a developer more often than not simply wants to 87 * set a string value into the header or content areas of the DialogPane. 88 * 89 * <p>It is important to understand the implications of setting non-null values 90 * in the {@link #headerProperty() header} and {@link #headerTextProperty() headerText} 91 * properties. The key points are as follows: 92 * 93 * <ol> 94 * <li>The {@code header} property takes precedence over the {@code headerText} 95 * property, so if both are set to non-null values, {@code header} will be 96 * used and {@code headerText} will be ignored.</li> 97 * <li>If {@code headerText} is set to a non-null value, and a 98 * {@link #graphicProperty() graphic} has also been set, the default position 99 * for the graphic shifts from being located to the left of the content area 100 * to being to the right of the header text.</li> 101 * <li>If {@code header} is set to a non-null value, and a 102 * {@link #graphicProperty() graphic} has also been set, the graphic is 103 * removed from its default position (to the left of the content area), 104 * and <strong>is not</strong> placed to the right of the custom header 105 * node. If the graphic is desired, it should be manually added in to the 106 * layout of the custom header node manually.</li> 107 * </ol> 108 * 109 * <p>DialogPane operates on the concept of {@link ButtonType}. A ButtonType is 110 * a descriptor of a single button that should be represented visually in the 111 * DialogPane. Developers who create a DialogPane therefore must specify the 112 * button types that they want to display, and this is done via the 113 * {@link #getButtonTypes()} method, which returns a modifiable 114 * {@link ObservableList}, which users can add to and remove from as desired. 115 * 116 * <p>The {@link ButtonType} class defines a number of pre-defined button types, 117 * such as {@link ButtonType#OK} and {@link ButtonType#CANCEL}. Many users of the 118 * JavaFX dialogs API will find that these pre-defined button types meet their 119 * needs, particularly due to their built-in support for 120 * {@link ButtonData#isDefaultButton() default} and 121 * {@link ButtonData#isCancelButton() cancel} buttons, as well as the benefit of 122 * the strings being translated into all languages which JavaFX is translated to. 123 * For users that want to define their own {@link ButtonType} (most commonly to 124 * define a button with custom text), they may do so via the constructors available 125 * on the {@link ButtonType} class. 126 * 127 * <p>Developers will quickly find that the amount of configurability offered 128 * via the {@link ButtonType} class is minimal. This is intentional, but does not 129 * mean that developers can not modify the buttons created by the {@link ButtonType} 130 * that have been specified. To do this, developers simply call the 131 * {@link #lookupButton(ButtonType)} method with the ButtonType (assuming it has 132 * already been set in the {@link #getButtonTypes()} list. The returned Node is 133 * typically of type {@link Button}, but this depends on if the 134 * {@link #createButton(ButtonType)} method has been overridden. 135 * 136 * <p>The DialogPane class offers a few methods that can be overridden by 137 * subclasses, to more easily enable custom functionality. These methods include 138 * the following: 139 * 140 * <ul> 141 * <li>{@link #createButton(ButtonType)} 142 * <li>{@link #createDetailsButton()} 143 * <li>{@link #createButtonBar()} 144 * </ul> 145 * 146 * <p>These methods are documented, so please take note of the expectations 147 * placed on any developer who wishes to override these methods with their own 148 * functionality. 149 * 150 * @see Dialog 151 * @since JavaFX 8u40 152 */ 153 @DefaultProperty("buttonTypes") 154 public class DialogPane extends Pane { 155 156 /************************************************************************** 157 * 158 * Static fields 159 * 160 **************************************************************************/ 161 162 /** 163 * Creates a Label node that works well within a Dialog. 164 * @param text The text to display 165 */ 166 static Label createContentLabel(String text) { 167 Label label = new Label(text); 168 label.setMaxWidth(Double.MAX_VALUE); 169 label.setMaxHeight(Double.MAX_VALUE); 170 label.getStyleClass().add("content"); 171 label.setWrapText(true); 172 label.setPrefWidth(360); 173 return label; 174 } 175 176 177 178 /************************************************************************** 179 * 180 * Private fields 181 * 182 **************************************************************************/ 183 184 private final GridPane headerTextPanel; 185 private final Label contentLabel; 186 private final StackPane graphicContainer; 187 private final Node buttonBar; 188 189 private final ObservableList<ButtonType> buttons = FXCollections.observableArrayList(); 190 191 private final Map<ButtonType, Node> buttonNodes = new WeakHashMap<>(); 192 193 private Node detailsButton; 194 195 // this is not a property - we have a package-scope setDialog method that 196 // sets this field. It is set by Dialog if the DialogPane is set inside a Dialog. 197 private Dialog<?> dialog; 198 199 200 201 /************************************************************************** 202 * 203 * Constructors 204 * 205 **************************************************************************/ 206 207 /** 208 * Creates a new DialogPane instance with a style class of 'dialog-pane'. 209 */ 210 public DialogPane() { 211 getStyleClass().add("dialog-pane"); 212 213 headerTextPanel = new GridPane(); 214 getChildren().add(headerTextPanel); 215 216 graphicContainer = new StackPane(); 217 218 contentLabel = createContentLabel(""); 219 getChildren().add(contentLabel); 220 221 buttonBar = createButtonBar(); 222 if (buttonBar != null) { 223 getChildren().add(buttonBar); 224 } 225 226 buttons.addListener((ListChangeListener<ButtonType>) c -> { 227 while (c.next()) { 228 if (c.wasRemoved()) { 229 for (ButtonType cmd : c.getRemoved()) { 230 buttonNodes.remove(cmd); 231 } 232 } 233 if (c.wasAdded()) { 234 for (ButtonType cmd : c.getAddedSubList()) { 235 if (! buttonNodes.containsKey(cmd)) { 236 buttonNodes.put(cmd, createButton(cmd)); 237 } 238 } 239 } 240 } 241 }); 242 } 243 244 245 246 /************************************************************************** 247 * 248 * Properties 249 * 250 **************************************************************************/ 251 252 // --- graphic 253 private final ObjectProperty<Node> graphicProperty = new StyleableObjectProperty<Node>() { 254 // The graphic is styleable by css, but it is the 255 // imageUrlProperty that handles the style value. 256 @Override public CssMetaData getCssMetaData() { 257 return StyleableProperties.GRAPHIC; 258 } 259 260 @Override public Object getBean() { 261 return DialogPane.this; 262 } 263 264 @Override public String getName() { 265 return "graphic"; 266 } 267 268 WeakReference<Node> graphicRef = new WeakReference<>(null); 269 270 protected void invalidated() { 271 Node oldGraphic = graphicRef.get(); 272 if (oldGraphic != null) { 273 getChildren().remove(oldGraphic); 274 } 275 276 Node newGraphic = getGraphic(); 277 graphicRef = new WeakReference<>(newGraphic); 278 updateHeaderArea(); 279 } 280 }; 281 282 /** 283 * The dialog graphic, presented either in the header, if one is showing, or 284 * to the left of the {@link #contentProperty() content}. 285 * 286 * @return An ObjectProperty wrapping the current graphic. 287 */ 288 public final ObjectProperty<Node> graphicProperty() { 289 return graphicProperty; 290 } 291 292 public final Node getGraphic() { 293 return graphicProperty.get(); 294 } 295 296 /** 297 * Sets the dialog graphic, which will be displayed either in the header, if 298 * one is showing, or to the left of the {@link #contentProperty() content}. 299 * 300 * @param graphic 301 * The new dialog graphic, or null if no graphic should be shown. 302 */ 303 public final void setGraphic(Node graphic) { 304 this.graphicProperty.set(graphic); 305 } 306 307 308 // --- imageUrl (this is NOT public API, except via CSS) 309 // Note that this code is a copy/paste from Labeled 310 private StyleableStringProperty imageUrl = null; 311 /** 312 * The imageUrl property is set from CSS and then the graphic property is 313 * set from the invalidated method. This ensures that the same image isn't 314 * reloaded. 315 */ 316 private StyleableStringProperty imageUrlProperty() { 317 if (imageUrl == null) { 318 imageUrl = new StyleableStringProperty() { 319 // 320 // If imageUrlProperty is invalidated, this is the origin of the style that 321 // triggered the invalidation. This is used in the invalidated() method where the 322 // value of super.getStyleOrigin() is not valid until after the call to set(v) returns, 323 // by which time invalidated will have been called. 324 // This value is initialized to USER in case someone calls set on the imageUrlProperty, which 325 // is possible: 326 // CssMetaData metaData = ((StyleableProperty)dialogPane.graphicProperty()).getCssMetaData(); 327 // StyleableProperty prop = metaData.getStyleableProperty(dialogPane); 328 // prop.set(someUrl); 329 // 330 // TODO: Note that prop != dialogPane, which violates the contract between StyleableProperty and CssMetaData. 331 // 332 StyleOrigin origin = StyleOrigin.USER; 333 334 @Override 335 public void applyStyle(StyleOrigin origin, String v) { 336 this.origin = origin; 337 338 // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value 339 if (graphicProperty == null || graphicProperty.isBound() == false) super.applyStyle(origin, v); 340 341 // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set. 342 this.origin = StyleOrigin.USER; 343 } 344 345 @Override 346 protected void invalidated() { 347 // need to call super.get() here since get() is overridden to return the graphicProperty's value 348 final String url = super.get(); 349 350 if (url == null) { 351 ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, null); 352 } else { 353 // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite. 354 final Node graphicNode = DialogPane.this.getGraphic(); 355 if (graphicNode instanceof ImageView) { 356 final ImageView imageView = (ImageView)graphicNode; 357 final Image image = imageView.getImage(); 358 if (image != null) { 359 final String imageViewUrl = image.getUrl(); 360 if (url.equals(imageViewUrl)) return; 361 } 362 363 } 364 365 final Image img = StyleManager.getInstance().getCachedImage(url); 366 367 if (img != null) { 368 // 369 // Note that it is tempting to try to re-use existing ImageView simply by setting 370 // the image on the current ImageView, if there is one. This would effectively change 371 // the image, but not the ImageView which means that no graphicProperty listeners would 372 // be notified. This is probably not what we want. 373 // 374 375 // 376 // Have to call applyStyle on graphicProperty so that the graphicProperty's 377 // origin matches the imageUrlProperty's origin. 378 // 379 ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, new ImageView(img)); 380 } 381 } 382 } 383 384 @Override 385 public String get() { 386 // 387 // The value of the imageUrlProperty is that of the graphicProperty. 388 // Return the value in a way that doesn't expand the graphicProperty. 389 // 390 final Node graphic = getGraphic(); 391 if (graphic instanceof ImageView) { 392 final Image image = ((ImageView)graphic).getImage(); 393 if (image != null) { 394 return image.getUrl(); 395 } 396 } 397 return null; 398 } 399 400 @Override 401 public StyleOrigin getStyleOrigin() { 402 // 403 // The origin of the imageUrlProperty is that of the graphicProperty. 404 // Return the origin in a way that doesn't expand the graphicProperty. 405 // 406 return graphicProperty != null ? ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty).getStyleOrigin() : null; 407 } 408 409 @Override 410 public Object getBean() { 411 return DialogPane.this; 412 } 413 414 @Override 415 public String getName() { 416 return "imageUrl"; 417 } 418 419 @Override 420 public CssMetaData<DialogPane,String> getCssMetaData() { 421 return StyleableProperties.GRAPHIC; 422 } 423 424 }; 425 } 426 return imageUrl; 427 } 428 429 430 // --- header 431 private final ObjectProperty<Node> header = new SimpleObjectProperty<Node>(null) { 432 WeakReference<Node> headerRef = new WeakReference<>(null); 433 @Override protected void invalidated() { 434 Node oldHeader = headerRef.get(); 435 if (oldHeader != null) { 436 getChildren().remove(oldHeader); 437 } 438 439 Node newHeader = getHeader(); 440 headerRef = new WeakReference<>(newHeader); 441 updateHeaderArea(); 442 } 443 }; 444 445 /** 446 * Node which acts as the dialog pane header. 447 * 448 * @return the header of the dialog pane. 449 */ 450 public final Node getHeader() { 451 return header.get(); 452 } 453 454 /** 455 * Assigns the dialog pane header. Any Node can be used. 456 * 457 * @param header The new header of the DialogPane. 458 */ 459 public final void setHeader(Node header) { 460 this.header.setValue(header); 461 } 462 463 /** 464 * Property representing the header area of the dialog pane. Note that if this 465 * header is set to a non-null value, that it will take up the entire top 466 * area of the DialogPane. It will also result in the DialogPane switching its 467 * layout to the 'header' layout - as outlined in the {@link DialogPane} class 468 * javadoc. 469 */ 470 public final ObjectProperty<Node> headerProperty() { 471 return header; 472 } 473 474 475 476 // --- header text 477 private final StringProperty headerText = new SimpleStringProperty(this, "headerText") { 478 @Override protected void invalidated() { 479 updateHeaderArea(); 480 requestLayout(); 481 } 482 }; 483 484 /** 485 * Sets the string to show in the dialog header area. Note that the header text 486 * is lower precedence than the {@link #headerProperty() header node}, meaning 487 * that if both the header node and the headerText properties are set, the 488 * header text will not be displayed in a default DialogPane instance. 489 * 490 * <p>When headerText is set to a non-null value, this will result in the 491 * DialogPane switching its layout to the 'header' layout - as outlined in 492 * the {@link DialogPane} class javadoc.</p> 493 */ 494 public final void setHeaderText(String headerText) { 495 this.headerText.set(headerText); 496 } 497 498 /** 499 * Returns the currently-set header text for this DialogPane. 500 */ 501 public final String getHeaderText() { 502 return headerText.get(); 503 } 504 505 /** 506 * A property representing the header text for the dialog pane. The header text 507 * is lower precedence than the {@link #headerProperty() header node}, meaning 508 * that if both the header node and the headerText properties are set, the 509 * header text will not be displayed in a default DialogPane instance. 510 * 511 * <p>When headerText is set to a non-null value, this will result in the 512 * DialogPane switching its layout to the 'header' layout - as outlined in 513 * the {@link DialogPane} class javadoc.</p> 514 */ 515 public final StringProperty headerTextProperty() { 516 return headerText; 517 } 518 519 520 // --- content 521 private final ObjectProperty<Node> content = new SimpleObjectProperty<Node>(null) { 522 WeakReference<Node> contentRef = new WeakReference<>(null); 523 @Override protected void invalidated() { 524 Node oldContent = contentRef.get(); 525 if (oldContent != null) { 526 getChildren().remove(oldContent); 527 } 528 529 Node newContent = getContent(); 530 contentRef = new WeakReference<>(newContent); 531 updateContentArea(); 532 } 533 }; 534 535 /** 536 * Returns the dialog content as a Node (even if it was set as a String 537 * using {@link #setContentText(String)} - this was simply transformed into a 538 * {@link Node} (most probably a {@link Label}). 539 * 540 * @return dialog's content 541 */ 542 public final Node getContent() { 543 return content.get(); 544 } 545 546 /** 547 * Assign dialog content. Any Node can be used 548 * 549 * @param content 550 * dialog's content 551 */ 552 public final void setContent(Node content) { 553 this.content.setValue(content); 554 } 555 556 /** 557 * Property representing the content area of the dialog. 558 */ 559 public final ObjectProperty<Node> contentProperty() { 560 return content; 561 } 562 563 564 // --- content text 565 private final StringProperty contentText = new SimpleStringProperty(this, "contentText") { 566 @Override protected void invalidated() { 567 updateContentArea(); 568 requestLayout(); 569 } 570 }; 571 572 /** 573 * Sets the string to show in the dialog content area. Note that the content text 574 * is lower precedence than the {@link #contentProperty() content node}, meaning 575 * that if both the content node and the contentText properties are set, the 576 * content text will not be displayed in a default DialogPane instance. 577 */ 578 public final void setContentText(String contentText) { 579 this.contentText.set(contentText); 580 } 581 582 /** 583 * Returns the currently-set content text for this DialogPane. 584 */ 585 public final String getContentText() { 586 return contentText.get(); 587 } 588 589 /** 590 * A property representing the content text for the dialog pane. The content text 591 * is lower precedence than the {@link #contentProperty() content node}, meaning 592 * that if both the content node and the contentText properties are set, the 593 * content text will not be displayed in a default DialogPane instance. 594 */ 595 public final StringProperty contentTextProperty() { 596 return contentText; 597 } 598 599 600 // --- expandable content 601 private final ObjectProperty<Node> expandableContentProperty = new SimpleObjectProperty<Node>(null) { 602 WeakReference<Node> expandableContentRef = new WeakReference<>(null); 603 @Override protected void invalidated() { 604 Node oldExpandableContent = expandableContentRef.get(); 605 if (oldExpandableContent != null) { 606 getChildren().remove(oldExpandableContent); 607 } 608 609 Node newExpandableContent = getExpandableContent(); 610 expandableContentRef = new WeakReference<Node>(newExpandableContent); 611 if (newExpandableContent != null) { 612 newExpandableContent.setVisible(isExpanded()); 613 newExpandableContent.setManaged(isExpanded()); 614 615 if (!newExpandableContent.getStyleClass().contains("expandable-content")) { //$NON-NLS-1$ 616 newExpandableContent.getStyleClass().add("expandable-content"); //$NON-NLS-1$ 617 } 618 619 getChildren().add(newExpandableContent); 620 } 621 } 622 }; 623 624 /** 625 * A property that represents the dialog expandable content area. Any Node 626 * can be placed in this area, but it will only be shown when the user 627 * clicks the 'Show Details' expandable button. This button will be added 628 * automatically when the expandable content property is non-null. 629 */ 630 public final ObjectProperty<Node> expandableContentProperty() { 631 return expandableContentProperty; 632 } 633 634 /** 635 * Returns the dialog expandable content node, if one is set, or null 636 * otherwise. 637 */ 638 public final Node getExpandableContent() { 639 return expandableContentProperty.get(); 640 } 641 642 /** 643 * Sets the dialog expandable content node, or null if no expandable content 644 * needs to be shown. 645 */ 646 public final void setExpandableContent(Node content) { 647 this.expandableContentProperty.set(content); 648 } 649 650 651 // --- expanded 652 private final BooleanProperty expandedProperty = new SimpleBooleanProperty(this, "expanded", false) { 653 protected void invalidated() { 654 final Node expandableContent = getExpandableContent(); 655 656 if (expandableContent != null) { 657 expandableContent.setVisible(isExpanded()); 658 } 659 660 requestLayout(); 661 } 662 }; 663 664 /** 665 * Represents whether the dialogPane is expanded. 666 */ 667 public final BooleanProperty expandedProperty() { 668 return expandedProperty; 669 } 670 671 /** 672 * Returns whether or not the dialogPane is expanded. 673 * 674 * @return true if dialogPane is expanded. 675 */ 676 public final boolean isExpanded() { 677 return expandedProperty().get(); 678 } 679 680 /** 681 * Sets whether the dialogPane is expanded. This only makes sense when there 682 * is {@link #expandableContentProperty() expandable content} to show. 683 * 684 * @param value true if dialogPane should be expanded. 685 */ 686 public final void setExpanded(boolean value) { 687 expandedProperty().set(value); 688 } 689 690 691 692 /************************************************************************** 693 * 694 * Public API 695 * 696 **************************************************************************/ 697 698 // --- button types 699 /** 700 * Observable list of button types used for the dialog button bar area 701 * (created via the {@link #createButtonBar()} method). Modifying the contents 702 * of this list will immediately change the buttons displayed to the user 703 * within the dialog pane. 704 * 705 * @return The {@link ObservableList} of {@link ButtonType button types} 706 * available to the user. 707 */ 708 public final ObservableList<ButtonType> getButtonTypes() { 709 return buttons; 710 } 711 712 /** 713 * This method provides a way in which developers may retrieve the actual 714 * Node for a given {@link ButtonType} (assuming it is part of the 715 * {@link #getButtonTypes() button types} list). 716 * 717 * @param buttonType The {@link ButtonType} for which a Node representation is requested. 718 * @return The Node used to represent the button type, as created by 719 * {@link #createButton(ButtonType)}, and only if the button type 720 * is part of the {@link #getButtonTypes() button types} list, otherwise null. 721 */ 722 public final Node lookupButton(ButtonType buttonType) { 723 return buttonNodes.get(buttonType); 724 } 725 726 /** 727 * This method can be overridden by subclasses to provide the button bar. 728 * Note that by overriding this method, the developer must take on multiple 729 * responsibilities: 730 * 731 * <ol> 732 * <li>The developer must immediately iterate through all 733 * {@link #getButtonTypes() button types} and call 734 * {@link #createButton(ButtonType)} for each of them in turn. 735 * <li>The developer must add a listener to the 736 * {@link #getButtonTypes() button types} list, and when this list changes 737 * update the button bar as appropriate. 738 * <li>Similarly, the developer must watch for changes to the 739 * {@link #expandableContentProperty() expandable content} property, 740 * adding and removing the details button (created via 741 * {@link #createDetailsButton()} method). 742 * </ol> 743 * 744 * <p>The default implementation of this method creates and returns a new 745 * {@link ButtonBar} instance. 746 */ 747 protected Node createButtonBar() { 748 ButtonBar buttonBar = new ButtonBar(); 749 buttonBar.setMaxWidth(Double.MAX_VALUE); 750 751 updateButtons(buttonBar); 752 getButtonTypes().addListener((ListChangeListener<? super ButtonType>) c -> updateButtons(buttonBar)); 753 expandableContentProperty().addListener(o -> updateButtons(buttonBar)); 754 755 return buttonBar; 756 } 757 758 /** 759 * This method can be overridden by subclasses to create a custom button that 760 * will subsequently inserted into the DialogPane button area (created via 761 * the {@link #createButtonBar()} method, but mostly commonly it is an instance 762 * of {@link ButtonBar}. 763 * 764 * @param buttonType The {@link ButtonType} to create a button from. 765 * @return A JavaFX {@link Node} that represents the given {@link ButtonType}, 766 * most commonly an instance of {@link Button}. 767 */ 768 protected Node createButton(ButtonType buttonType) { 769 final Button button = new Button(buttonType.getText()); 770 final ButtonData buttonData = buttonType.getButtonData(); 771 ButtonBar.setButtonData(button, buttonData); 772 button.setDefaultButton(buttonData.isDefaultButton()); 773 button.setCancelButton(buttonData.isCancelButton()); 774 button.addEventHandler(ActionEvent.ACTION, ae -> { 775 if (ae.isConsumed()) return; 776 if (dialog != null) { 777 dialog.setResultAndClose(buttonType, true); 778 } 779 }); 780 781 return button; 782 } 783 784 /** 785 * This method can be overridden by subclasses to create a custom details button. 786 * 787 * <p>To override this method you must do two things: 788 * <ol> 789 * <li>The button will need to have its own code set to handle mouse / keyboard 790 * interaction and to toggle the state of the 791 * {@link #expandedProperty() expanded} property. 792 * <li>If your button changes its visuals based on whether the dialog pane 793 * is expanded or collapsed, you should add a listener to the 794 * {@link #expandedProperty() expanded} property, so that you may update 795 * the button visuals. 796 * </ol> 797 */ 798 protected Node createDetailsButton() { 799 final Hyperlink detailsButton = new Hyperlink(); 800 final String moreText = ControlResources.getString("Dialog.detail.button.more"); //$NON-NLS-1$ 801 final String lessText = ControlResources.getString("Dialog.detail.button.less"); //$NON-NLS-1$ 802 803 InvalidationListener expandedListener = o -> { 804 final boolean isExpanded = isExpanded(); 805 detailsButton.setText(isExpanded ? lessText : moreText); 806 detailsButton.getStyleClass().setAll("details-button", (isExpanded ? "less" : "more")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 807 }; 808 809 // we call the listener immediately to ensure the state is correct at start up 810 expandedListener.invalidated(null); 811 expandedProperty().addListener(expandedListener); 812 813 detailsButton.setOnAction(ae -> setExpanded(!isExpanded())); 814 return detailsButton; 815 } 816 817 private double oldHeight = -1; 818 819 /** {@inheritDoc} */ 820 @Override protected void layoutChildren() { 821 final boolean hasHeader = hasHeader(); 822 823 // snapped insets code commented out to resolve RT-39738 824 final double w = Math.max(minWidth(-1), getWidth());// - (snappedLeftInset() + snappedRightInset()); 825 826 final double minHeight = minHeight(w); 827 final double prefHeight = prefHeight(w); 828 final double maxHeight = maxHeight(w); 829 final double currentHeight = getHeight(); 830 final double dialogHeight = dialog == null ? 0 : dialog.dialog.getSceneHeight(); 831 double h; 832 833 if (prefHeight > currentHeight && prefHeight > minHeight && (prefHeight <= dialogHeight || dialogHeight == 0)) { 834 h = prefHeight; 835 resize(w, h); 836 } else { 837 boolean isDialogGrowing = currentHeight > oldHeight; 838 839 if (isDialogGrowing) { 840 double _h = currentHeight < prefHeight ? 841 Math.min(prefHeight, currentHeight) : Math.max(prefHeight, dialogHeight); 842 h = Utils.boundedSize(_h, minHeight, maxHeight); 843 } else { 844 h = Utils.boundedSize(Math.min(currentHeight, dialogHeight), minHeight, maxHeight); 845 } 846 resize(w, h); 847 } 848 849 h -= (snappedTopInset() + snappedBottomInset()); 850 851 oldHeight = h; 852 853 final double leftPadding = snappedLeftInset(); 854 final double topPadding = snappedTopInset(); 855 final double rightPadding = snappedRightInset(); 856 857 // create the nodes up front so we can work out sizing 858 final Node header = getActualHeader(); 859 final Node content = getActualContent(); 860 final Node graphic = getActualGraphic(); 861 final Node expandableContent = getExpandableContent(); 862 863 final double graphicPrefWidth = hasHeader || graphic == null ? 0 : graphic.prefWidth(-1); 864 final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0; 865 final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w); 866 final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(-1); 867 868 final double expandableContentPrefHeight; 869 final double contentAreaHeight; 870 final double contentAndGraphicHeight; 871 872 final double availableContentWidth = w - graphicPrefWidth - leftPadding - rightPadding; 873 874 if (isExpanded()) { 875 // precedence goes to content and then expandable content 876 contentAreaHeight = isExpanded() ? content.prefHeight(availableContentWidth) : 0; 877 contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight); 878 expandableContentPrefHeight = h - (headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight); 879 } else { 880 // content gets the lowest precedence 881 expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0; 882 contentAreaHeight = h - (headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight); 883 contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight); 884 } 885 886 double x = leftPadding; 887 double y = topPadding; 888 889 if (! hasHeader) { 890 if (graphic != null) { 891 graphic.resizeRelocate(x, y, graphicPrefWidth, graphicPrefHeight); 892 x += graphicPrefWidth; 893 } 894 } else { 895 header.resizeRelocate(x, y, w - (leftPadding + rightPadding), headerPrefHeight); 896 y += headerPrefHeight; 897 } 898 899 content.resizeRelocate(x, y, availableContentWidth, contentAreaHeight); 900 y += hasHeader ? contentAreaHeight : contentAndGraphicHeight; 901 902 if (expandableContent != null) { 903 expandableContent.resizeRelocate(leftPadding, y, w - rightPadding, expandableContentPrefHeight); 904 y += expandableContentPrefHeight; 905 } 906 907 if (buttonBar != null) { 908 buttonBar.resizeRelocate(leftPadding, 909 y, 910 w - (leftPadding + rightPadding), 911 buttonBarPrefHeight); 912 } 913 } 914 915 /** {@inheritDoc} */ 916 @Override protected double computeMinWidth(double height) { 917 double headerMinWidth = hasHeader() ? getActualHeader().minWidth(height) + 10 : 0; 918 double contentMinWidth = getActualContent().minWidth(height); 919 double buttonBarMinWidth = buttonBar == null ? 0 : buttonBar.minWidth(height); 920 double graphicMinWidth = getActualGraphic().minWidth(height); 921 922 double expandableContentMinWidth = 0; 923 final Node expandableContent = getExpandableContent(); 924 if (isExpanded() && expandableContent != null) { 925 expandableContentMinWidth = expandableContent.minWidth(height); 926 } 927 928 double minWidth = snappedLeftInset() + 929 (hasHeader() ? 0 : graphicMinWidth) + 930 Math.max(Math.max(headerMinWidth, expandableContentMinWidth), Math.max(contentMinWidth, buttonBarMinWidth)) + 931 snappedRightInset(); 932 933 return snapSize(minWidth); 934 } 935 936 /** {@inheritDoc} */ 937 @Override protected double computeMinHeight(double width) { 938 final boolean hasHeader = hasHeader(); 939 940 double headerMinHeight = hasHeader ? getActualHeader().minHeight(width) : 0; 941 double buttonBarMinHeight = buttonBar == null ? 0 : buttonBar.minHeight(width); 942 943 Node graphic = getActualGraphic(); 944 double graphicMinWidth = hasHeader ? 0 : graphic.minWidth(-1); 945 double graphicMinHeight = hasHeader ? 0 : graphic.minHeight(width); 946 947 // min height of a label is based on one line (wrapping is ignored) 948 Node content = getActualContent(); 949 double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE : 950 hasHeader ? width : (width - graphicMinWidth); 951 double contentMinHeight = content.minHeight(contentAvailableWidth); 952 953 double expandableContentMinHeight = 0; 954 final Node expandableContent = getExpandableContent(); 955 if (isExpanded() && expandableContent != null) { 956 expandableContentMinHeight = expandableContent.minHeight(width); 957 } 958 959 double minHeight = snappedTopInset() + 960 headerMinHeight + 961 Math.max(graphicMinHeight, contentMinHeight) + 962 expandableContentMinHeight + 963 buttonBarMinHeight + 964 snappedBottomInset(); 965 966 return snapSize(minHeight); 967 } 968 969 /** {@inheritDoc} */ 970 @Override protected double computePrefWidth(double height) { 971 double headerPrefWidth = hasHeader() ? getActualHeader().prefWidth(height) + 10 : 0; 972 double contentPrefWidth = getActualContent().prefWidth(height); 973 double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height); 974 double graphicPrefWidth = getActualGraphic().prefWidth(height); 975 976 double expandableContentPrefWidth = 0; 977 final Node expandableContent = getExpandableContent(); 978 if (isExpanded() && expandableContent != null) { 979 expandableContentPrefWidth = expandableContent.prefWidth(height); 980 } 981 982 double prefWidth = snappedLeftInset() + 983 (hasHeader() ? 0 : graphicPrefWidth) + 984 Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) + 985 snappedRightInset(); 986 987 return snapSize(prefWidth); 988 } 989 990 /** {@inheritDoc} */ 991 @Override protected double computePrefHeight(double width) { 992 final boolean hasHeader = hasHeader(); 993 994 double headerPrefHeight = hasHeader ? getActualHeader().prefHeight(width) : 0; 995 double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width); 996 997 Node graphic = getActualGraphic(); 998 double graphicPrefWidth = hasHeader ? 0 : graphic.prefWidth(-1); 999 double graphicPrefHeight = hasHeader ? 0 : graphic.prefHeight(width); 1000 1001 Node content = getActualContent(); 1002 double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE : 1003 hasHeader ? width : (width - graphicPrefWidth); 1004 double contentPrefHeight = content.prefHeight(contentAvailableWidth); 1005 1006 double expandableContentPrefHeight = 0; 1007 final Node expandableContent = getExpandableContent(); 1008 if (isExpanded() && expandableContent != null) { 1009 expandableContentPrefHeight = expandableContent.prefHeight(width); 1010 } 1011 1012 double prefHeight = snappedTopInset() + 1013 headerPrefHeight + 1014 Math.max(graphicPrefHeight, contentPrefHeight) + 1015 expandableContentPrefHeight + 1016 buttonBarPrefHeight + 1017 snappedBottomInset(); 1018 1019 return snapSize(prefHeight); 1020 } 1021 1022 1023 1024 /************************************************************************** 1025 * 1026 * Private implementation 1027 * @param buttonBar 1028 * 1029 **************************************************************************/ 1030 1031 private void updateButtons(ButtonBar buttonBar) { 1032 buttonBar.getButtons().clear(); 1033 1034 // show details button if expandable content is present 1035 if (hasExpandableContent()) { 1036 if (detailsButton == null) { 1037 detailsButton = createDetailsButton(); 1038 } 1039 ButtonBar.setButtonData(detailsButton, ButtonData.HELP_2); 1040 buttonBar.getButtons().add(detailsButton); 1041 ButtonBar.setButtonUniformSize(detailsButton, false); 1042 } 1043 1044 boolean hasDefault = false; 1045 for (ButtonType cmd : getButtonTypes()) { 1046 Node button = buttonNodes.computeIfAbsent(cmd, dialogButton -> createButton(cmd)); 1047 1048 // keep only first default button 1049 if (button instanceof Button) { 1050 ButtonData buttonType = cmd.getButtonData(); 1051 1052 ((Button)button).setDefaultButton(!hasDefault && buttonType != null && buttonType.isDefaultButton()); 1053 ((Button)button).setCancelButton(buttonType != null && buttonType.isCancelButton()); 1054 1055 hasDefault |= buttonType != null && buttonType.isDefaultButton(); 1056 } 1057 buttonBar.getButtons().add(button); 1058 } 1059 } 1060 1061 private Node getActualContent() { 1062 Node content = getContent(); 1063 return content == null ? contentLabel : content; 1064 } 1065 1066 private Node getActualHeader() { 1067 Node header = getHeader(); 1068 return header == null ? headerTextPanel : header; 1069 } 1070 1071 private Node getActualGraphic() { 1072 return headerTextPanel; 1073 } 1074 1075 private void updateHeaderArea() { 1076 Node header = getHeader(); 1077 if (header != null) { 1078 if (! getChildren().contains(header)) { 1079 getChildren().add(header); 1080 } 1081 1082 headerTextPanel.setVisible(false); 1083 headerTextPanel.setManaged(false); 1084 } else { 1085 final String headerText = getHeaderText(); 1086 1087 headerTextPanel.getChildren().clear(); 1088 headerTextPanel.getStyleClass().clear(); 1089 1090 // recreate the headerTextNode and add it to the children list. 1091 headerTextPanel.setMaxWidth(Double.MAX_VALUE); 1092 1093 if (headerText != null && ! headerText.isEmpty()) { 1094 headerTextPanel.getStyleClass().add("header-panel"); //$NON-NLS-1$ 1095 } 1096 1097 // on left of header is the text 1098 Label headerLabel = new Label(headerText); 1099 headerLabel.setWrapText(true); 1100 headerLabel.setAlignment(Pos.CENTER_LEFT); 1101 headerLabel.setMaxWidth(Double.MAX_VALUE); 1102 headerLabel.setMaxHeight(Double.MAX_VALUE); 1103 headerTextPanel.add(headerLabel, 0, 0); 1104 1105 // on the right of the header is a graphic, if one is specified 1106 graphicContainer.getChildren().clear(); 1107 1108 if (! graphicContainer.getStyleClass().contains("graphic-container")) { //$NON-NLS-1$) 1109 graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$ 1110 } 1111 1112 final Node graphic = getGraphic(); 1113 if (graphic != null) { 1114 graphicContainer.getChildren().add(graphic); 1115 } 1116 headerTextPanel.add(graphicContainer, 1, 0); 1117 1118 // column constraints 1119 ColumnConstraints textColumn = new ColumnConstraints(); 1120 textColumn.setFillWidth(true); 1121 textColumn.setHgrow(Priority.ALWAYS); 1122 ColumnConstraints graphicColumn = new ColumnConstraints(); 1123 graphicColumn.setFillWidth(false); 1124 graphicColumn.setHgrow(Priority.NEVER); 1125 headerTextPanel.getColumnConstraints().setAll(textColumn , graphicColumn); 1126 1127 headerTextPanel.setVisible(true); 1128 headerTextPanel.setManaged(true); 1129 } 1130 } 1131 1132 private void updateContentArea() { 1133 Node content = getContent(); 1134 if (content != null) { 1135 if (! getChildren().contains(content)) { 1136 getChildren().add(content); 1137 } 1138 1139 if (! content.getStyleClass().contains("content")) { 1140 content.getStyleClass().add("content"); 1141 } 1142 1143 contentLabel.setVisible(false); 1144 contentLabel.setManaged(false); 1145 } else { 1146 final String contentText = getContentText(); 1147 final boolean visible = contentText != null && !contentText.isEmpty(); 1148 contentLabel.setText(visible ? contentText : ""); 1149 contentLabel.setVisible(visible); 1150 contentLabel.setManaged(visible); 1151 } 1152 } 1153 1154 boolean hasHeader() { 1155 return getHeader() != null || isTextHeader(); 1156 } 1157 1158 private boolean isTextHeader() { 1159 String headerText = getHeaderText(); 1160 return headerText != null && !headerText.isEmpty(); 1161 } 1162 1163 boolean hasExpandableContent() { 1164 return getExpandableContent() != null; 1165 } 1166 1167 void setDialog(Dialog<?> dialog) { 1168 this.dialog = dialog; 1169 } 1170 1171 1172 1173 /*************************************************************************** 1174 * * 1175 * Stylesheet Handling * 1176 * * 1177 **************************************************************************/ 1178 1179 /** 1180 * @treatAsPrivate implementation detail 1181 */ 1182 private static class StyleableProperties { 1183 1184 private static final CssMetaData<DialogPane,String> GRAPHIC = 1185 new CssMetaData<DialogPane,String>("-fx-graphic", 1186 StringConverter.getInstance()) { 1187 1188 @Override 1189 public boolean isSettable(DialogPane n) { 1190 // Note that we care about the graphic, not imageUrl 1191 return n.graphicProperty == null || !n.graphicProperty.isBound(); 1192 } 1193 1194 @Override 1195 public StyleableProperty<String> getStyleableProperty(DialogPane n) { 1196 return n.imageUrlProperty(); 1197 } 1198 }; 1199 1200 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1201 static { 1202 final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData()); 1203 Collections.addAll(styleables, 1204 GRAPHIC 1205 ); 1206 STYLEABLES = Collections.unmodifiableList(styleables); 1207 } 1208 } 1209 1210 /** 1211 * @return The CssMetaData associated with this class, which may include the 1212 * CssMetaData of its super classes. 1213 */ 1214 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1215 return StyleableProperties.STYLEABLES; 1216 } 1217 1218 /** {@inheritDoc} */ 1219 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 1220 return getClassCssMetaData(); 1221 } 1222 }