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