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