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