1 /*
   2  * Copyright (c) 2010, 2014, 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 
  26 package com.sun.javafx.scene.control.skin;
  27 
  28 import javafx.application.Platform;
  29 import javafx.beans.InvalidationListener;
  30 import javafx.geometry.HPos;
  31 import javafx.geometry.NodeOrientation;
  32 import javafx.geometry.Orientation;
  33 import javafx.geometry.Pos;
  34 import javafx.geometry.VPos;
  35 import javafx.scene.AccessibleAttribute;
  36 import javafx.scene.Node;
  37 import javafx.scene.Scene;
  38 import javafx.scene.control.ContentDisplay;
  39 import javafx.scene.control.Control;
  40 import javafx.scene.control.Label;
  41 import javafx.scene.control.Labeled;
  42 import javafx.scene.control.OverrunStyle;
  43 import javafx.scene.input.KeyCombination;
  44 import javafx.scene.input.Mnemonic;
  45 import javafx.scene.shape.Line;
  46 import javafx.scene.shape.Rectangle;
  47 import javafx.scene.text.Font;
  48 
  49 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  50 import com.sun.javafx.scene.control.behavior.TextBinding;
  51 
  52 import static javafx.scene.control.ContentDisplay.*;
  53 import static javafx.scene.control.OverrunStyle.*;
  54 import javafx.scene.image.ImageView;
  55 
  56 public abstract class LabeledSkinBase<C extends Labeled, B extends BehaviorBase<C>> extends BehaviorSkinBase<C, B> {
  57 
  58     /**
  59      *  The Text node used to display the text. This is package only
  60      *  for the sake of testing!
  61      */
  62     LabeledText text;
  63 
  64     /**
  65      * Indicates that the text content is invalid and needs to be updated.
  66      * This is package private only for the sake of testing.
  67      */
  68     boolean invalidText = true;
  69 
  70     /**
  71      * A reference to the last-known graphic on the Labeled. This reference
  72      * is kept so that we can remove listeners from the old graphic later
  73      */
  74     Node graphic;
  75 
  76     /**
  77      * The cached full width of the non-truncated text. We only want to
  78      * recompute this if the text has itself changed, or if the font has changed.
  79      * This is package private ONLY FOR THE SAKE OF TESTING
  80      */
  81     double textWidth = Double.NEGATIVE_INFINITY;
  82 
  83     /**
  84      * The cached width of the ellipsis string. This will be recomputed
  85      * if the font or the ellipsisString property have changed.
  86      * This is package private ONLY FOR THE SAKE OF TESTING
  87      */
  88     double ellipsisWidth = Double.NEGATIVE_INFINITY;
  89 
  90     /**
  91      * A listener which is applied to the graphic whenever the graphic is set
  92      * and is visible within the labeled. For example, if there is a graphic
  93      * defined on the Labeled but the ContentDisplay is set to TEXT_ONLY, then
  94      * we will not bother installing this listener on the graphic. In all
  95      * other cases, if the graphic is defined, it will have this listener
  96      * added to it, which ensures that if the graphic's layout bounds change,
  97      * we end up performing a layout and potentially update the visible text.
  98      *
  99      * This is package private ONLY FOR THE SAKE OF TESTING
 100      */
 101     final InvalidationListener graphicPropertyChangedListener = valueModel -> {
 102         invalidText = true;
 103         getSkinnable().requestLayout();
 104     };
 105 
 106     private Rectangle textClip;
 107     private double wrapWidth;
 108     private double wrapHeight;
 109 
 110 
 111     /**
 112      * Constructor for LabeledSkinBase. The Labeled must be specified, and cannot be null.
 113      * At the conclusion of the constructor call, the skin will be marked as
 114      * needsLayout, and will be fully configured based on the current state of
 115      * the labeled. Any subsequent changes to the Labeled will be handled via
 116      * listeners and applied appropriately.
 117      *
 118      * @param labeled
 119      */
 120     public LabeledSkinBase(final C labeled, final B behavior) {
 121         super(labeled, behavior);
 122 
 123         // Configure the Text node with all of the attributes from the
 124         // Labeled which apply to it.
 125         text = new LabeledText(labeled);
 126         
 127         updateChildren();
 128 
 129         // Labels do not block the mouse by default, unlike most other UI Controls.
 130         //consumeMouseEvents(false);
 131 
 132         // Register listeners
 133         registerChangeListener(labeled.ellipsisStringProperty(), "ELLIPSIS_STRING");
 134         registerChangeListener(labeled.widthProperty(), "WIDTH");
 135         registerChangeListener(labeled.heightProperty(), "HEIGHT");
 136         registerChangeListener(labeled.textFillProperty(), "TEXT_FILL");
 137         registerChangeListener(labeled.fontProperty(), "FONT");
 138         registerChangeListener(labeled.graphicProperty(), "GRAPHIC");
 139         registerChangeListener(labeled.contentDisplayProperty(), "CONTENT_DISPLAY");
 140         registerChangeListener(labeled.labelPaddingProperty(), "LABEL_PADDING");
 141         registerChangeListener(labeled.graphicTextGapProperty(), "GRAPHIC_TEXT_GAP");
 142         registerChangeListener(labeled.alignmentProperty(), "ALIGNMENT");
 143         registerChangeListener(labeled.mnemonicParsingProperty(), "MNEMONIC_PARSING");
 144         registerChangeListener(labeled.textProperty(), "TEXT");
 145         registerChangeListener(labeled.textAlignmentProperty(), "TEXT_ALIGNMENT");
 146         registerChangeListener(labeled.textOverrunProperty(), "TEXT_OVERRUN");
 147         registerChangeListener(labeled.wrapTextProperty(), "WRAP_TEXT");
 148         registerChangeListener(labeled.underlineProperty(), "UNDERLINE");
 149         registerChangeListener(labeled.lineSpacingProperty(), "LINE_SPACING");
 150         registerChangeListener(labeled.sceneProperty(), "SCENE");
 151     }
 152 
 153     /***************************************************************************
 154      *                                                                         *
 155      * Control State Changes                                                   *
 156      *                                                                         *
 157      **************************************************************************/
 158 
 159     @Override protected void handleControlPropertyChanged(String p) {
 160         super.handleControlPropertyChanged(p);
 161         /*
 162          * There are basically 2 things to worry about in each of these handlers
 163          *  1) Update the Text node
 164          *  2) Have the text metrics changed?
 165          *
 166          * If the metrics have changed, we need to request a layout and invalidate
 167          * the text so that we recompute the display text on next read.
 168          */
 169         if ("WIDTH".equals(p)) {
 170             updateWrappingWidth();
 171             invalidText = true;
 172             // No requestLayout() because Control will force a layout
 173         } else if ("HEIGHT".equals(p)) {
 174             invalidText = true;
 175             // No requestLayout() because Control will force a layout
 176         } else if ("FONT".equals(p)) {
 177             textMetricsChanged();
 178             invalidateWidths();
 179             ellipsisWidth = Double.NEGATIVE_INFINITY;
 180         } else if ("GRAPHIC".equals(p)) {
 181             updateChildren();
 182             textMetricsChanged();
 183         } else if ("CONTENT_DISPLAY".equals(p)) {
 184             updateChildren();
 185             textMetricsChanged();
 186         } else if ("LABEL_PADDING".equals(p)) {
 187             textMetricsChanged();
 188         } else if ("GRAPHIC_TEXT_GAP".equals(p)) {
 189             textMetricsChanged();
 190         } else if ("ALIGNMENT".equals(p)) {
 191             // Doesn't involve text metrics because if the text is too long, then
 192             // it will already have fit all available width and a change to hpos
 193             // has no effect. Or it is too short (i.e. it all fits) and we don't
 194             // have to worry about truncation. So just call request layout.
 195             // Doesn't involve text metrics because if the text is too long, then
 196             // it will already have fit all available height and a change to vpos
 197             // has no effect. Or it is too short (i.e. it all fits) and we don't
 198             // have to worry about truncation. So just call request layout.
 199             getSkinnable().requestLayout();
 200         } else if ("MNEMONIC_PARSING".equals(p)) {
 201             containsMnemonic = false;
 202             textMetricsChanged();
 203         } else if ("TEXT".equals(p)) {
 204             updateChildren();
 205             textMetricsChanged();
 206             invalidateWidths();
 207         } else if ("TEXT_ALIGNMENT".equals(p)) {
 208             // NO-OP
 209         } else if ("TEXT_OVERRUN".equals(p)) {
 210             textMetricsChanged();
 211         } else if ("ELLIPSIS_STRING".equals(p)) {
 212             textMetricsChanged();
 213             invalidateWidths();
 214             ellipsisWidth = Double.NEGATIVE_INFINITY;
 215         } else if ("WRAP_TEXT".equals(p)) {
 216             updateWrappingWidth();
 217             textMetricsChanged();
 218         } else if ("UNDERLINE".equals(p)) {
 219             textMetricsChanged();
 220         } else if ("LINE_SPACING".equals(p)) {
 221             textMetricsChanged();
 222         } else if ("SCENE".equals(p)) {
 223             sceneChanged();
 224         }
 225     }
 226 
 227     protected double topLabelPadding() { // TODOJASPER remove these if you can
 228         return snapSize(getSkinnable().getLabelPadding().getTop());
 229     }
 230 
 231     protected double bottomLabelPadding() {
 232         return snapSize(getSkinnable().getLabelPadding().getBottom());
 233     }
 234 
 235     protected double leftLabelPadding() {
 236         return snapSize(getSkinnable().getLabelPadding().getLeft());
 237     }
 238 
 239     protected double rightLabelPadding() {
 240         return snapSize(getSkinnable().getLabelPadding().getRight());
 241     }
 242 
 243 
 244     /**
 245      * Called whenever some state has changed that affects the text metrics.
 246      * Changes here will involve invalidating the display text so the next
 247      * call to updateDisplayedText computes a new value, and call requestLayout.
 248      */
 249     private void textMetricsChanged() {
 250         invalidText = true;
 251         getSkinnable().requestLayout();
 252     }
 253 
 254     /*
 255     ** The Label is a mnemonic, and it's target node
 256     ** has changed, but it's label hasn't so just
 257     ** swap them over, and tidy up.
 258     */
 259     protected void mnemonicTargetChanged() {
 260         if (containsMnemonic == true) {
 261             /*
 262             ** was there previously a labelFor  
 263             */
 264             removeMnemonic();
 265 
 266             /*
 267             ** is there a new labelFor
 268             */
 269             Control control = getSkinnable();
 270             if (control instanceof Label) {
 271                 labeledNode = ((Label)control).getLabelFor();
 272                 addMnemonic();
 273             }
 274             else {
 275                 labeledNode = null;
 276             }
 277         }
 278     }
 279 
 280     private void sceneChanged() {
 281         final Labeled labeled = getSkinnable();
 282         Scene scene = labeled.getScene();
 283 
 284         if (scene != null && containsMnemonic) {
 285             addMnemonic();
 286         }
 287 
 288     }
 289 
 290     /**
 291      * Marks minWidth as being invalid and in need of recomputation.
 292      */
 293     private void invalidateWidths() {
 294         textWidth = Double.NEGATIVE_INFINITY;
 295     }
 296 
 297     /**
 298      * Updates the content of the underlying Text node. This method should
 299      * only be called when necessary. If the invalidText flag is not set, then
 300      * the method is a no-op. This care is taken because recomputing the
 301      * text to display is an expensive operation. Package private ONLY FOR THE
 302      * SAKE OF TESTING.
 303      */
 304     void updateDisplayedText() {
 305         updateDisplayedText(-1, -1);
 306     }
 307 
 308     private void updateDisplayedText(double w, double h) {
 309         if (invalidText) {
 310             final Labeled labeled = getSkinnable();
 311             String s = labeled.getText();
 312 
 313             int mnemonicIndex = -1;
 314 
 315             /*
 316             ** if there's a valid string then parse it
 317             */
 318             if (s != null && s.length() > 0) {
 319                 bindings = new TextBinding(s);
 320 
 321                 if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) {
 322                     /*
 323                     ** the Labeled has a MnemonicParsing property,
 324                     ** if set true, then auto-parsing will check for
 325                     ** a mnemonic
 326                     */
 327                     if (labeled instanceof Label) {
 328                         // buttons etc
 329                         labeledNode = ((Label)labeled).getLabelFor();
 330                     } else {
 331                         labeledNode = labeled;
 332                     }
 333 
 334                     if (labeledNode == null) {
 335                         labeledNode = labeled;
 336                     }
 337                     mnemonicIndex = bindings.getMnemonicIndex() ;
 338                 }
 339             }
 340 
 341             /*
 342             ** we were previously a mnemonic
 343             */
 344             if (containsMnemonic) {
 345                 /*
 346                 ** are we no longer a mnemonic, or have we changed code?
 347                 */
 348                 if (mnemonicScene != null) {
 349                     if (mnemonicIndex == -1 ||
 350                             (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) {
 351                         removeMnemonic();
 352                     }
 353                     containsMnemonic = false;
 354                 }
 355             }
 356             else {
 357                 /*
 358                 ** this can happen if mnemonic parsing is
 359                 ** disabled on a previously valid mnemonic
 360                 */
 361                 removeMnemonic();
 362             }
 363 
 364             /*
 365             ** check we have a labeled
 366             */
 367             if (s != null && s.length() > 0) {
 368                 if (mnemonicIndex >= 0 && containsMnemonic == false) {
 369                     containsMnemonic = true;
 370                     mnemonicCode = bindings.getMnemonicKeyCombination();
 371                     addMnemonic();
 372                 }
 373             }
 374 
 375             if (containsMnemonic == true) {
 376                 s = bindings.getText();
 377                 if (mnemonic_underscore == null) {
 378                     mnemonic_underscore = new Line();
 379                     mnemonic_underscore.setStartX(0.0f);
 380                     mnemonic_underscore.setStartY(0.0f);
 381                     mnemonic_underscore.setEndY(0.0f);
 382                     mnemonic_underscore.getStyleClass().clear();
 383                     mnemonic_underscore.getStyleClass().setAll("mnemonic-underline");
 384                 }
 385                 if (!getChildren().contains(mnemonic_underscore)) {
 386                     getChildren().add(mnemonic_underscore);
 387                 }
 388             } else {
 389                 /*
 390                 ** we don't need a mnemonic....
 391                 */
 392                 if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) {
 393                     s = bindings.getText();
 394                 }
 395                 else {
 396                     s = labeled.getText();
 397                 }
 398                 if (mnemonic_underscore != null) {
 399                     if (getChildren().contains(mnemonic_underscore)) {
 400                         Platform.runLater(() -> {
 401                               getChildren().remove(mnemonic_underscore);
 402                               mnemonic_underscore = null;
 403                         });
 404                     }
 405                 }
 406             }
 407 
 408             int len = s != null ? s.length() : 0;
 409             boolean multiline = false;
 410 
 411             if (s != null && len > 0) {
 412                 int i = s.indexOf('\n');
 413                 if (i > -1 && i < len - 1) {
 414                     // Multiline text with embedded newlines - not
 415                     // taking into account a potential trailing newline.
 416                     multiline = true;
 417                 }
 418             }
 419 
 420             String result;
 421             boolean horizontalPosition =
 422                     (labeled.getContentDisplay() == ContentDisplay.LEFT ||
 423                     labeled.getContentDisplay() == ContentDisplay.RIGHT);
 424 
 425             double availableWidth = labeled.getWidth() - snappedLeftInset() - leftLabelPadding() -
 426                                     snappedRightInset() - rightLabelPadding();
 427             availableWidth = Math.max(availableWidth, 0);
 428 
 429             if (w == -1) {
 430                 w = availableWidth;
 431             }
 432             double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth);
 433             if (horizontalPosition && !isIgnoreGraphic()) {
 434                 double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap());
 435                 w -= graphicW;
 436                 minW -= graphicW;
 437             }
 438             wrapWidth = Math.max(minW, w);
 439 
 440             boolean verticalPosition =
 441                     (labeled.getContentDisplay() == ContentDisplay.TOP ||
 442                     labeled.getContentDisplay() == ContentDisplay.BOTTOM);
 443 
 444             double availableHeight = labeled.getHeight() - snappedTopInset() - topLabelPadding() -
 445                                      snappedBottomInset() - bottomLabelPadding();
 446             availableHeight = Math.max(availableHeight, 0);
 447 
 448             if (h == -1) {
 449                 h = availableHeight;
 450             }
 451             double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight);
 452             if (verticalPosition && labeled.getGraphic() != null) {
 453                 double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap();
 454                 h -= graphicH;
 455                 minH -= graphicH;
 456             }
 457             wrapHeight = Math.max(minH, h);
 458 
 459             updateWrappingWidth();
 460 
 461             Font font = text.getFont();
 462             OverrunStyle truncationStyle = labeled.getTextOverrun();
 463             String ellipsisString = labeled.getEllipsisString();
 464 
 465             if (labeled.isWrapText()) {
 466                 result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType());
 467             } else if (multiline) {
 468                 StringBuilder sb = new StringBuilder();
 469 
 470                 String[] splits = s.split("\n");
 471                 for (int i = 0; i < splits.length; i++) {
 472                     sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString));
 473                     if (i < splits.length - 1) {
 474                         sb.append('\n');
 475                     }
 476                 }
 477 
 478                 // TODO: Consider what to do in the case where vertical space is
 479                 // limited and the last visible line isn't already truncated
 480                 // with a trailing ellipsis. What if the style calls for leading
 481                 // or center ellipses? We could possibly add an additional
 482                 // trailing ellipsis to the last visible line, like this:
 483 
 484                 // +--------------------------------+
 485                 // |  This is some long text with multiple lines\n
 486                 // |  where more than one exceed the|width\n
 487                 // |  and wrapText is false, and all|lines\n
 488                 // +--don't fit.--------------------+
 489                 //
 490                 // +--------------------------------+
 491                 // |  This is some...multiple lines |
 492                 // |  where more t...ceed the width |
 493                 // |  and wrapText...d all lines... |
 494                 // +--------------------------------+
 495 
 496                 result = sb.toString();
 497             } else {
 498                 result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString);
 499             }
 500 
 501             if (result != null && result.endsWith("\n")) {
 502                 // Strip ending newline so we don't display another row.
 503                 result = result.substring(0, result.length() - 1);
 504             }
 505 
 506             text.setText(result);
 507             updateWrappingWidth();
 508             invalidText = false;
 509         }
 510     }
 511 
 512     private void addMnemonic() {
 513         if (labeledNode != null) {
 514             mnemonicScene = labeledNode.getScene();
 515             if (mnemonicScene != null) {
 516                 mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode));
 517             }
 518         }
 519     }
 520 
 521 
 522     private void removeMnemonic() {
 523         if (mnemonicScene != null && labeledNode != null) {
 524             mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode));
 525             mnemonicScene = null;
 526         }
 527     }
 528 
 529     /**
 530      * Updates the wrapping width of the text node. Although changing the font
 531      * does affect the metrics used for text layout, this method does not
 532      * call requestLayout or invalidate the text, since it may be called
 533      * from the constructor and such work would be duplicative and wasted.
 534      */
 535     private void updateWrappingWidth() {
 536         final Labeled labeled = getSkinnable();
 537         text.setWrappingWidth(0);
 538         if (labeled.isWrapText()) {
 539             // Note that the wrapping width needs to be set to zero before
 540             // getting the text's real preferred width.
 541             double w = Math.min(text.prefWidth(-1), wrapWidth);
 542             text.setWrappingWidth(w);
 543         }
 544     }
 545 
 546     /**
 547      * Updates the children managed by LabeledSkinBase, which can be the Labeled
 548      * graphic and/or a Text node. Only those nodes which actually must
 549      * be used are used. For example, with a ContentDisplay of
 550      * GRAPHIC_ONLY the text node is not added, and with a ContentDisplay
 551      * of TEXT_ONLY, the graphic is not added.
 552      */
 553     protected void updateChildren() {
 554         final Labeled labeled = getSkinnable();
 555         // Only in some situations do we want to have the graphicPropertyChangedListener
 556         // installed. Since updateChildren() is not called much, we'll just remove it always
 557         // and reinstall it later if it is necessary to do so.
 558         if (graphic != null) {
 559             graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener);
 560         }
 561         // Now update the graphic (since it may have changed)
 562         graphic = labeled.getGraphic();
 563         
 564         // RT-19851 Only setMouseTransparent(true) for an ImageView.  This allows the button 
 565         // to be picked regardless of the changing images on top of it. 
 566         if (graphic instanceof ImageView) {
 567             graphic.setMouseTransparent(true);
 568         }                
 569         
 570         // Now update the children (and add the graphicPropertyChangedListener as necessary)
 571         if (isIgnoreGraphic()) {
 572             if (labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) {
 573                 getChildren().clear();
 574             } else {
 575                 getChildren().setAll(text);
 576             }
 577         } else {
 578             graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener);
 579             if (isIgnoreText()) {
 580                 getChildren().setAll(graphic);
 581             } else {
 582                 getChildren().setAll(graphic, text);
 583             }
 584 
 585             // RT-37420
 586             graphic.impl_processCSS(false);
 587 
 588         }
 589 
 590     }
 591 
 592     /**
 593      * Gets whether for various computations we can ignore the presence of the graphic
 594      * (or lack thereof). 
 595      * @return
 596      */
 597     protected boolean isIgnoreGraphic() {
 598         return (graphic == null ||
 599                 !graphic.isManaged() ||
 600                 getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY);
 601     }
 602 
 603     /**
 604      * Gets whether for various computations we can ignore the presence of the text.
 605      * @return
 606      */
 607     protected boolean isIgnoreText() {
 608         final Labeled labeled = getSkinnable();
 609         final String txt = labeled.getText();
 610         return (txt == null ||
 611                 txt.equals("") ||
 612                 labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY);
 613     }
 614 
 615     /***************************************************************************
 616      *                                                                         *
 617      * Skin Layout                                                             *
 618      *                                                                         *
 619      **************************************************************************/
 620 
 621     /**
 622      * Compute and return the minimum width of this Labeled. The minimum width is
 623      * the smaller of the width of "..." and the width with the actual text.
 624      * In this way, if the text width itself is smaller than the ellipsis then
 625      * we should use that as the min width, otherwise the ellipsis needs to be the
 626      * min width.
 627      * <p>
 628      * We use the same calculation here regardless of whether we are talking
 629      * about a single or multiline labeled. So a multiline labeled may find that
 630      * the width of the "..." is as small as it will ever get.
 631      */
 632     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 633         return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset);
 634     }
 635 
 636     private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 637         // First compute the minTextWidth by checking the width of the string
 638         // made by the ellipsis "...", and then by checking the width of the
 639         // string made up by labeled.text. We want the smaller of the two.
 640         final Labeled labeled = getSkinnable();
 641         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 642         final double gap = labeled.getGraphicTextGap();
 643         double minTextWidth = 0;
 644 
 645         final Font font = text.getFont();
 646         OverrunStyle truncationStyle = labeled.getTextOverrun();
 647         String ellipsisString = labeled.getEllipsisString();
 648         final String string = labeled.getText();
 649         final boolean emptyText = string == null || string.isEmpty();
 650 
 651         if (!emptyText) {
 652             // We only want to recompute the full text width if the font or text changed
 653             if (truncationStyle == CLIP) {
 654                 if (textWidth == Double.NEGATIVE_INFINITY) {
 655                     // Show at minimum the first character
 656                     textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0);
 657                 }
 658                 minTextWidth = textWidth;
 659             } else {
 660                 if (textWidth == Double.NEGATIVE_INFINITY) {
 661                     textWidth = Utils.computeTextWidth(font, string, 0);
 662                 }
 663                 // We only want to recompute the ellipsis width if the font has changed
 664                 if (ellipsisWidth == Double.NEGATIVE_INFINITY) {
 665                     ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0);
 666                 }
 667                 minTextWidth = Math.min(textWidth, ellipsisWidth);
 668             }
 669         }
 670 
 671         // Now inspect the graphic and the hpos to determine the the minWidth
 672         final Node graphic = labeled.getGraphic();
 673         double width;
 674         if (isIgnoreGraphic()) {
 675             width = minTextWidth;
 676         } else if (isIgnoreText()) {
 677             width = graphic.minWidth(-1);
 678         } else if (contentDisplay == LEFT || contentDisplay == RIGHT){
 679             width = (minTextWidth + graphic.minWidth(-1) + gap);
 680         } else {
 681             width = Math.max(minTextWidth, graphic.minWidth(-1));
 682         }
 683 
 684         return width + leftInset + leftLabelPadding() +
 685                 rightInset + rightLabelPadding();
 686     }
 687 
 688     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 689         return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset);
 690     }
 691 
 692     private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 693         final Labeled labeled = getSkinnable();
 694         final Font font = text.getFont();
 695 
 696         String str = labeled.getText();
 697         if (str != null && str.length() > 0) {
 698             int newlineIndex = str.indexOf('\n');
 699             if (newlineIndex >= 0) {
 700                 str = str.substring(0, newlineIndex);
 701             }
 702         }
 703 
 704         // TODO figure out how to cache this effectively.
 705         // Base minimum height on one line (ignoring wrapping here).
 706         double s = labeled.getLineSpacing();
 707         final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType());
 708 
 709         double h = textHeight;
 710 
 711         // Now we want to add on the graphic if necessary!
 712         if (!isIgnoreGraphic()) {
 713             final Node graphic = labeled.getGraphic();
 714             if (labeled.getContentDisplay() == ContentDisplay.TOP
 715                 || labeled.getContentDisplay() == ContentDisplay.BOTTOM) {
 716                 h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight;
 717             } else {
 718                 h = Math.max(textHeight, graphic.minHeight(width));
 719             }
 720         }
 721 
 722         return topInset + h + bottomInset + topLabelPadding() - bottomLabelPadding();
 723     }
 724 
 725     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 726         // Get the preferred width of the text
 727         final Labeled labeled = getSkinnable();
 728         final Font font = text.getFont();
 729         final String string = labeled.getText();
 730         boolean emptyText = string == null || string.isEmpty();
 731         double widthPadding = leftInset + leftLabelPadding() +
 732                               rightInset + rightLabelPadding();
 733 
 734         double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0);
 735 
 736         // Fix for RT-39889
 737         double graphicWidth = graphic == null ? 0.0 :
 738                 Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1));
 739 
 740         // Now add on the graphic, gap, and padding as appropriate
 741         final Node graphic = labeled.getGraphic();
 742         if (isIgnoreGraphic()) {
 743             return textWidth + widthPadding;
 744         } else if (isIgnoreText()) {
 745             return graphicWidth + widthPadding;
 746         } else if (labeled.getContentDisplay() == ContentDisplay.LEFT
 747                 || labeled.getContentDisplay() == ContentDisplay.RIGHT) {
 748             return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding;
 749         } else {
 750             return Math.max(textWidth, graphicWidth) + widthPadding;
 751         }
 752     }
 753 
 754     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 755         final Labeled labeled = getSkinnable();
 756         final Font font = text.getFont();
 757         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 758         final double gap = labeled.getGraphicTextGap();
 759         width -= leftInset + leftLabelPadding() +
 760                 rightInset + rightLabelPadding();
 761 
 762         String str = labeled.getText();
 763         if (str != null && str.endsWith("\n")) {
 764             // Strip ending newline so we don't count another row.
 765             str = str.substring(0, str.length() - 1);
 766         }
 767 
 768         double textWidth = width;
 769         if (!isIgnoreGraphic() &&
 770             (contentDisplay == LEFT || contentDisplay == RIGHT)) {
 771             textWidth -= (graphic.prefWidth(-1) + gap);
 772         }
 773 
 774         // TODO figure out how to cache this effectively.
 775         final double textHeight = Utils.computeTextHeight(font, str,
 776                                                           labeled.isWrapText() ? textWidth : 0,
 777                                                           labeled.getLineSpacing(), text.getBoundsType());
 778 
 779         // Now we want to add on the graphic if necessary!
 780         double h = textHeight;
 781         if (!isIgnoreGraphic()) {
 782             final Node graphic = labeled.getGraphic();
 783             if (contentDisplay == TOP || contentDisplay == BOTTOM) {
 784                 h = graphic.prefHeight(width) + gap + textHeight;
 785             } else {
 786                 h = Math.max(textHeight, graphic.prefHeight(width));
 787             }
 788         }
 789 
 790         return topInset + h + bottomInset + topLabelPadding() + bottomLabelPadding();
 791     }
 792 
 793     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 794         return getSkinnable().prefWidth(height);
 795     }
 796 
 797     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 798         return getSkinnable().prefHeight(width);
 799     }
 800 
 801     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 802         double textBaselineOffset = text.getBaselineOffset();
 803         double h = textBaselineOffset;
 804         final Labeled labeled = getSkinnable();
 805         final Node g = labeled.getGraphic();
 806         if (!isIgnoreGraphic()) {
 807             ContentDisplay contentDisplay = labeled.getContentDisplay();
 808             if (contentDisplay == ContentDisplay.TOP) {
 809                 h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset;
 810             } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) {
 811                 h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2;
 812             }
 813         }
 814                 
 815         return topInset + topLabelPadding() + h;
 816     }
 817 
 818     public TextBinding bindings;
 819     Line mnemonic_underscore;
 820 
 821     private boolean containsMnemonic = false;
 822     private Scene mnemonicScene = null;
 823     private KeyCombination mnemonicCode;
 824     // needs to be an object, as MenuItem isn't a node
 825     private Node labeledNode = null;
 826 
 827     /**
 828      * The Layout algorithm works like this:
 829      *
 830      *  - Get the labeled w/h, graphic w/h, text w/h
 831      *  - Compute content w/h based on graphicVPos, graphicHPos,
 832      *    graphicTextGap, and graphic w/h and text w/h
 833      *  - (Note that the text content has been pre-truncated where
 834      *    necessary)
 835      *  - compute content x/y based on content w/h and labeled w/h
 836      *    and the labeled's hpos and vpos
 837      *  - position the graphic and text
 838      */
 839     @Override protected void layoutChildren(final double x, final double y,
 840             final double w, final double h) {
 841         layoutLabelInArea(x, y, w, h);
 842     }
 843 
 844     /**
 845      * Performs the actual layout of the label content within the area given.
 846      * This method is called by subclasses that override layoutChildren().
 847      *
 848      * @param x The x position of the label part of the control, inside padding
 849      *
 850      * @param y The y position of the label part of the control, inside padding
 851      *
 852      * @param w The width of the label part of the control, not including padding
 853      *
 854      * @param h The height of the label part of the control, not including padding
 855      */
 856     protected void layoutLabelInArea(double x, double y, double w, double h) {
 857         layoutLabelInArea(x, y, w, h, null);
 858     }
 859 
 860     /**
 861      * Performs the actual layout of the label content within the area given.
 862      * This method is called by subclasses that override layoutChildren().
 863      *
 864      * @param x The x position of the label part of the control, inside padding
 865      *
 866      * @param y The y position of the label part of the control, inside padding
 867      *
 868      * @param w The width of the label part of the control, not including padding
 869      *
 870      * @param h The height of the label part of the control, not including padding
 871      *
 872      * @param alignment The alignment of the label part of the control within the given area. If null, then the control's alignment will be used.
 873      */
 874     protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) {
 875         // References to essential labeled state
 876         final Labeled labeled = getSkinnable();
 877         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 878 
 879         if (alignment == null) {
 880             alignment = labeled.getAlignment();
 881         }
 882 
 883         final HPos hpos = alignment == null ? HPos.LEFT   : alignment.getHpos();
 884         final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos();
 885 
 886         // Figure out whether we should ignore the Graphic, and/or
 887         // ignore the Text
 888         final boolean ignoreGraphic = isIgnoreGraphic();
 889         final boolean ignoreText = isIgnoreText();
 890 
 891         x += leftLabelPadding();
 892         y += topLabelPadding();
 893         w -= leftLabelPadding() + rightLabelPadding();
 894         h -= topLabelPadding() + bottomLabelPadding();
 895 
 896         // Compute some standard useful numbers for the graphic, text, and gap
 897         double graphicWidth;
 898         double graphicHeight;
 899         double textWidth;
 900         double textHeight;
 901 
 902         if (ignoreGraphic) {
 903             graphicWidth = graphicHeight = 0;
 904         } else if (ignoreText) {
 905             if (graphic.isResizable()) {
 906                 Orientation contentBias = graphic.getContentBias();
 907                 if (contentBias == Orientation.HORIZONTAL) {
 908                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1));
 909                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth));
 910                 } else if (contentBias == Orientation.VERTICAL) {
 911                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1));
 912                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight));
 913                 } else {
 914                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1));
 915                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1));
 916                 }
 917                 graphic.resize(graphicWidth, graphicHeight);
 918             } else {
 919                 graphicWidth = graphic.getLayoutBounds().getWidth();
 920                 graphicHeight = graphic.getLayoutBounds().getHeight();
 921             }
 922         } else {
 923             graphic.autosize(); // We have to do this before getting metrics
 924             graphicWidth = graphic.getLayoutBounds().getWidth();
 925             graphicHeight = graphic.getLayoutBounds().getHeight();
 926         }
 927 
 928         if (ignoreText) {
 929             textWidth  = textHeight = 0;
 930             text.setText("");
 931         } else {
 932             updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed
 933             textWidth  = snapSize(Math.min(text.getLayoutBounds().getWidth(),  wrapWidth));
 934             textHeight = snapSize(Math.min(text.getLayoutBounds().getHeight(), wrapHeight));
 935         }
 936 
 937         final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap();
 938 
 939         // Figure out the contentWidth and contentHeight. This is the width
 940         // and height of the Labeled and Graphic together, not the available
 941         // content area (which would be a different calculation).
 942         double contentWidth = Math.max(graphicWidth, textWidth);
 943         double contentHeight = Math.max(graphicHeight, textHeight);
 944         if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) {
 945             contentHeight = graphicHeight + gap + textHeight;
 946         } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) {
 947             contentWidth = graphicWidth + gap + textWidth;
 948         }
 949 
 950         // Now we want to compute the x/y location to place the content at.
 951 
 952          // Compute the contentX position based on hpos and the space available
 953         double contentX;
 954         if (hpos == HPos.LEFT) {
 955            contentX = x;
 956         } else if (hpos == HPos.RIGHT) {
 957            contentX = x + (w - contentWidth);
 958         } else {
 959             // TODO Baseline may not be handled correctly
 960             // may have been CENTER or null, treat as center
 961             contentX = (x + ((w - contentWidth) / 2.0));
 962         }
 963 
 964         // Compute the contentY position based on vpos and the space available
 965         double contentY;
 966         if (vpos == VPos.TOP) {
 967             contentY = y;
 968         } else if (vpos == VPos.BOTTOM) {
 969             contentY = (y + (h - contentHeight));
 970         } else {
 971             // TODO Baseline may not be handled correctly
 972             // may have been CENTER, BASELINE, or null, treat as center
 973             contentY = (y + ((h - contentHeight) / 2.0));
 974         }
 975 
 976         double preMnemonicWidth = 0.0;
 977         double mnemonicWidth = 0.0;
 978         double mnemonicHeight = 0.0;
 979         if (containsMnemonic) {
 980             final Font font = text.getFont();
 981             String preSt = bindings.getText();
 982             preMnemonicWidth = Utils.computeTextWidth(font, preSt.substring(0,bindings.getMnemonicIndex()) , 0);
 983             mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(),bindings.getMnemonicIndex()+1) , 0);
 984             mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType());
 985         }
 986 
 987 
 988         // Now to position the graphic and text. At this point I know the
 989         // contentX and contentY locations (including the padding and whatnot
 990         // that was defined on the Labeled). I also know the content width and
 991         // height. So now I just need to lay out the graphic and text within
 992         // that content x/y/w/h area.
 993         if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) {
 994             text.setManaged(true);
 995         }
 996 
 997         if (ignoreGraphic && ignoreText) {
 998             // There might be a text node as a child, or a graphic node as
 999             // a child. However we don't have to do anything for the graphic
1000             // node because the only way it can be a child and still have
1001             // ignoreGraphic true is if it is unmanaged. Text however might
1002             // be a child but still not matter, in which case we will just
1003             // stop managing it (although really I wish it just wasn't here
1004             // all all in that case)
1005             if (text.isManaged()) {
1006                 text.setManaged(false);
1007             }
1008             text.relocate(snapPosition(contentX), snapPosition(contentY));
1009         } else if (ignoreGraphic) {
1010             // Since I only have to position the text, it goes at the
1011             // contentX/contentY location. Note that positionNode will
1012             // adjust the text based on the text's minX/minY so no need to
1013             // worry about that here
1014             text.relocate(snapPosition(contentX), snapPosition(contentY));
1015             if (containsMnemonic) {
1016                 mnemonic_underscore.setEndX(mnemonicWidth-2.0);
1017                 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1);
1018             }
1019 
1020         } else if (ignoreText) {
1021             // there isn't text to display, so we need to position it
1022             // such that it doesn't affect the content area (although when
1023             // there is a graphic, the text isn't even in the scene)
1024             text.relocate(snapPosition(contentX), snapPosition(contentY));
1025             graphic.relocate(snapPosition(contentX), snapPosition(contentY));
1026             if (containsMnemonic) {
1027                 mnemonic_underscore.setEndX(mnemonicWidth);
1028                 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
1029                 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1);
1030 
1031             }
1032         } else {
1033             // There is both text and a graphic, so I need to position them
1034             // relative to each other
1035             double graphicX = 0;
1036             double graphicY = 0;
1037             double textX = 0;
1038             double textY = 0;
1039 
1040             if (contentDisplay == ContentDisplay.TOP) {
1041                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
1042                 textX = contentX + ((contentWidth - textWidth) / 2.0);
1043                 // The graphic is above the text, so it is positioned at
1044                 // graphicY and the text below it.
1045                 graphicY = contentY;
1046                 textY = graphicY + graphicHeight + gap;
1047             } else if (contentDisplay == ContentDisplay.RIGHT) {
1048                 // The graphic is to the right of the text
1049                 textX = contentX;
1050                 graphicX = textX + textWidth + gap;
1051                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
1052                 textY = contentY + ((contentHeight - textHeight) / 2.0);
1053             } else if (contentDisplay == ContentDisplay.BOTTOM) {
1054                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
1055                 textX = contentX + ((contentWidth - textWidth) / 2.0);
1056                 // The graphic is below the text
1057                 textY = contentY;
1058                 graphicY = textY + textHeight + gap;
1059             } else if (contentDisplay == ContentDisplay.LEFT) {
1060                 // The graphic is to the left of the text, so the graphicX is
1061                 // simply the contentX and the textX is to the right of it.
1062                 graphicX = contentX;
1063                 textX = graphicX + graphicWidth + gap;
1064                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
1065                 textY = contentY + ((contentHeight - textHeight) / 2.0);
1066             } else if (contentDisplay == ContentDisplay.CENTER) {
1067                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
1068                 textX = contentX + ((contentWidth - textWidth) / 2.0);
1069                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
1070                 textY = contentY + ((contentHeight - textHeight) / 2.0);
1071             }
1072             text.relocate(snapPosition(textX), snapPosition(textY));
1073             if (containsMnemonic) {
1074                 mnemonic_underscore.setEndX(mnemonicWidth);
1075                 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
1076                 mnemonic_underscore.relocate(snapPosition(textX+preMnemonicWidth), snapPosition(textY+mnemonicHeight-1));
1077             }
1078             graphic.relocate(snapPosition(graphicX), snapPosition(graphicY));
1079         }
1080 
1081         /**
1082          * check if the label text overflows it's bounds.
1083          * If there's an overflow, and no text clip then
1084          * we'll clip it. 
1085          * If there is no overflow, and the label text has a
1086          * clip, then remove it.
1087          */
1088         if ((text != null) && 
1089             ((text.getLayoutBounds().getHeight() > wrapHeight) ||
1090              (text.getLayoutBounds().getWidth() > wrapWidth))) {
1091 
1092             if (textClip == null) {
1093                 textClip = new Rectangle();
1094             }
1095 
1096             if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) {
1097                 textClip.setX(text.getLayoutBounds().getMinX());
1098             } else {
1099                 textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth);
1100             }
1101             textClip.setY(text.getLayoutBounds().getMinY());
1102             textClip.setWidth(wrapWidth);
1103             textClip.setHeight(wrapHeight);
1104             if (text.getClip() == null) {
1105                 text.setClip(textClip);
1106             }
1107         }
1108         else {
1109             /**
1110              * content fits inside bounds, no need
1111              * for a clip
1112              */
1113             if (text.getClip() != null) {
1114                 text.setClip(null);
1115             }
1116         }
1117     }
1118 
1119     @Override
1120     protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1121         switch (attribute) {
1122             case TEXT: {
1123                 Labeled labeled = getSkinnable();
1124                 String accText = labeled.getAccessibleText();
1125                 if (accText != null && !accText.isEmpty()) return accText;
1126 
1127                 /* Use the text in the binding if available to handle mnemonics */
1128                 if (bindings != null) {
1129                     String text = bindings.getText(); 
1130                     if (text != null && !text.isEmpty()) return text;
1131                 }
1132                 /* Avoid the content in text.getText() as it can contain ellipses 
1133                  * for clipping
1134                  */
1135                 if (labeled != null) {
1136                     String text = labeled.getText(); 
1137                     if (text != null && !text.isEmpty()) return text;
1138                 }
1139                 /* Use the graphic as last resource. Note that this implementation
1140                  * does not attempt to combine the label and graphics if both
1141                  * are being displayed
1142                  */
1143                 if (graphic != null) {
1144                     Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT);
1145                     if (result != null) return result;
1146                 }
1147                 return null;
1148             }
1149             case MNEMONIC: {
1150                 if (bindings != null) {
1151                     return bindings.getMnemonic();
1152                 }
1153                 return null;
1154             }
1155             default: return super.queryAccessibleAttribute(attribute, parameters);
1156         }
1157     }
1158 }