1 /*
   2  * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  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         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             // RT-37420
 282             graphic.impl_processCSS(false);
 283 
 284         }
 285 
 286     }
 287 
 288     /**
 289      * Compute and return the minimum width of this Labeled. The minimum width is
 290      * the smaller of the width of "..." and the width with the actual text.
 291      * In this way, if the text width itself is smaller than the ellipsis then
 292      * we should use that as the min width, otherwise the ellipsis needs to be the
 293      * min width.
 294      * <p>
 295      * We use the same calculation here regardless of whether we are talking
 296      * about a single or multiline labeled. So a multiline labeled may find that
 297      * the width of the "..." is as small as it will ever get.
 298      */
 299     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 300         return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset);
 301     }
 302 
 303     /** {@inheritDoc} */
 304     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 305         return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset);
 306     }
 307 
 308     /** {@inheritDoc} */
 309     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 310         // Get the preferred width of the text
 311         final Labeled labeled = getSkinnable();
 312         final Font font = text.getFont();
 313         final String string = labeled.getText();
 314         boolean emptyText = string == null || string.isEmpty();
 315         double widthPadding = leftInset + leftLabelPadding() +
 316                 rightInset + rightLabelPadding();
 317 
 318         double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0);
 319 
 320         // Fix for RT-39889
 321         double graphicWidth = graphic == null ? 0.0 :
 322                 Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1));
 323 
 324         // Now add on the graphic, gap, and padding as appropriate
 325         final Node graphic = labeled.getGraphic();
 326         if (isIgnoreGraphic()) {
 327             return textWidth + widthPadding;
 328         } else if (isIgnoreText()) {
 329             return graphicWidth + widthPadding;
 330         } else if (labeled.getContentDisplay() == ContentDisplay.LEFT
 331                 || labeled.getContentDisplay() == ContentDisplay.RIGHT) {
 332             return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding;
 333         } else {
 334             return Math.max(textWidth, graphicWidth) + widthPadding;
 335         }
 336     }
 337 
 338     /** {@inheritDoc} */
 339     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 340         final Labeled labeled = getSkinnable();
 341         final Font font = text.getFont();
 342         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 343         final double gap = labeled.getGraphicTextGap();
 344         width -= leftInset + leftLabelPadding() +
 345                 rightInset + rightLabelPadding();
 346 
 347         String str = labeled.getText();
 348         if (str != null && str.endsWith("\n")) {
 349             // Strip ending newline so we don't count another row.
 350             str = str.substring(0, str.length() - 1);
 351         }
 352 
 353         double textWidth = width;
 354         if (!isIgnoreGraphic() &&
 355                 (contentDisplay == LEFT || contentDisplay == RIGHT)) {
 356             textWidth -= (graphic.prefWidth(-1) + gap);
 357         }
 358 
 359         // TODO figure out how to cache this effectively.
 360         final double textHeight = Utils.computeTextHeight(font, str,
 361                 labeled.isWrapText() ? textWidth : 0,
 362                 labeled.getLineSpacing(), text.getBoundsType());
 363 
 364         // Now we want to add on the graphic if necessary!
 365         double h = textHeight;
 366         if (!isIgnoreGraphic()) {
 367             final Node graphic = labeled.getGraphic();
 368             if (contentDisplay == TOP || contentDisplay == BOTTOM) {
 369                 h = graphic.prefHeight(width) + gap + textHeight;
 370             } else {
 371                 h = Math.max(textHeight, graphic.prefHeight(width));
 372             }
 373         }
 374 
 375         return topInset + h + bottomInset + topLabelPadding() + bottomLabelPadding();
 376     }
 377 
 378     /** {@inheritDoc} */
 379     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 380         return getSkinnable().prefWidth(height);
 381     }
 382 
 383     /** {@inheritDoc} */
 384     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 385         return getSkinnable().prefHeight(width);
 386     }
 387 
 388     /** {@inheritDoc} */
 389     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 390         double textBaselineOffset = text.getBaselineOffset();
 391         double h = textBaselineOffset;
 392         final Labeled labeled = getSkinnable();
 393         final Node g = labeled.getGraphic();
 394         if (!isIgnoreGraphic()) {
 395             ContentDisplay contentDisplay = labeled.getContentDisplay();
 396             if (contentDisplay == ContentDisplay.TOP) {
 397                 h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset;
 398             } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) {
 399                 h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2;
 400             }
 401         }
 402 
 403         return topInset + topLabelPadding() + h;
 404     }
 405 
 406     /**
 407      * The Layout algorithm works like this:
 408      *
 409      *  - Get the labeled w/h, graphic w/h, text w/h
 410      *  - Compute content w/h based on graphicVPos, graphicHPos,
 411      *    graphicTextGap, and graphic w/h and text w/h
 412      *  - (Note that the text content has been pre-truncated where
 413      *    necessary)
 414      *  - compute content x/y based on content w/h and labeled w/h
 415      *    and the labeled's hpos and vpos
 416      *  - position the graphic and text
 417      */
 418     @Override protected void layoutChildren(final double x, final double y,
 419                                             final double w, final double h) {
 420         layoutLabelInArea(x, y, w, h);
 421     }
 422 
 423     /**
 424      * Performs the actual layout of the label content within the area given.
 425      * This method is called by subclasses that override layoutChildren().
 426      *
 427      * @param x The x position of the label part of the control, inside padding
 428      *
 429      * @param y The y position of the label part of the control, inside padding
 430      *
 431      * @param w The width of the label part of the control, not including padding
 432      *
 433      * @param h The height of the label part of the control, not including padding
 434      */
 435     protected void layoutLabelInArea(double x, double y, double w, double h) {
 436         layoutLabelInArea(x, y, w, h, null);
 437     }
 438 
 439     /**
 440      * Performs the actual layout of the label content within the area given.
 441      * This method is called by subclasses that override layoutChildren().
 442      *
 443      * @param x The x position of the label part of the control, inside padding
 444      *
 445      * @param y The y position of the label part of the control, inside padding
 446      *
 447      * @param w The width of the label part of the control, not including padding
 448      *
 449      * @param h The height of the label part of the control, not including padding
 450      *
 451      * @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.
 452      */
 453     protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) {
 454         // References to essential labeled state
 455         final Labeled labeled = getSkinnable();
 456         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 457 
 458         if (alignment == null) {
 459             alignment = labeled.getAlignment();
 460         }
 461 
 462         final HPos hpos = alignment == null ? HPos.LEFT   : alignment.getHpos();
 463         final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos();
 464 
 465         // Figure out whether we should ignore the Graphic, and/or
 466         // ignore the Text
 467         final boolean ignoreGraphic = isIgnoreGraphic();
 468         final boolean ignoreText = isIgnoreText();
 469 
 470         x += leftLabelPadding();
 471         y += topLabelPadding();
 472         w -= leftLabelPadding() + rightLabelPadding();
 473         h -= topLabelPadding() + bottomLabelPadding();
 474 
 475         // Compute some standard useful numbers for the graphic, text, and gap
 476         double graphicWidth;
 477         double graphicHeight;
 478         double textWidth;
 479         double textHeight;
 480 
 481         if (ignoreGraphic) {
 482             graphicWidth = graphicHeight = 0;
 483         } else if (ignoreText) {
 484             if (graphic.isResizable()) {
 485                 Orientation contentBias = graphic.getContentBias();
 486                 if (contentBias == Orientation.HORIZONTAL) {
 487                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1));
 488                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth));
 489                 } else if (contentBias == Orientation.VERTICAL) {
 490                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1));
 491                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight));
 492                 } else {
 493                     graphicWidth  = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1));
 494                     graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1));
 495                 }
 496                 graphic.resize(graphicWidth, graphicHeight);
 497             } else {
 498                 graphicWidth = graphic.getLayoutBounds().getWidth();
 499                 graphicHeight = graphic.getLayoutBounds().getHeight();
 500             }
 501         } else {
 502             graphic.autosize(); // We have to do this before getting metrics
 503             graphicWidth = graphic.getLayoutBounds().getWidth();
 504             graphicHeight = graphic.getLayoutBounds().getHeight();
 505         }
 506 
 507         if (ignoreText) {
 508             textWidth  = textHeight = 0;
 509             text.setText("");
 510         } else {
 511             updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed
 512             textWidth  = snapSize(Math.min(text.getLayoutBounds().getWidth(),  wrapWidth));
 513             textHeight = snapSize(Math.min(text.getLayoutBounds().getHeight(), wrapHeight));
 514         }
 515 
 516         final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap();
 517 
 518         // Figure out the contentWidth and contentHeight. This is the width
 519         // and height of the Labeled and Graphic together, not the available
 520         // content area (which would be a different calculation).
 521         double contentWidth = Math.max(graphicWidth, textWidth);
 522         double contentHeight = Math.max(graphicHeight, textHeight);
 523         if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) {
 524             contentHeight = graphicHeight + gap + textHeight;
 525         } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) {
 526             contentWidth = graphicWidth + gap + textWidth;
 527         }
 528 
 529         // Now we want to compute the x/y location to place the content at.
 530 
 531         // Compute the contentX position based on hpos and the space available
 532         double contentX;
 533         if (hpos == HPos.LEFT) {
 534             contentX = x;
 535         } else if (hpos == HPos.RIGHT) {
 536             contentX = x + (w - contentWidth);
 537         } else {
 538             // TODO Baseline may not be handled correctly
 539             // may have been CENTER or null, treat as center
 540             contentX = (x + ((w - contentWidth) / 2.0));
 541         }
 542 
 543         // Compute the contentY position based on vpos and the space available
 544         double contentY;
 545         if (vpos == VPos.TOP) {
 546             contentY = y;
 547         } else if (vpos == VPos.BOTTOM) {
 548             contentY = (y + (h - contentHeight));
 549         } else {
 550             // TODO Baseline may not be handled correctly
 551             // may have been CENTER, BASELINE, or null, treat as center
 552             contentY = (y + ((h - contentHeight) / 2.0));
 553         }
 554 
 555         double preMnemonicWidth = 0.0;
 556         double mnemonicWidth = 0.0;
 557         double mnemonicHeight = 0.0;
 558         if (containsMnemonic) {
 559             final Font font = text.getFont();
 560             String preSt = bindings.getText();
 561             preMnemonicWidth = Utils.computeTextWidth(font, preSt.substring(0, bindings.getMnemonicIndex()), 0);
 562             mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(), bindings.getMnemonicIndex() + 1), 0);
 563             mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType());
 564         }
 565 
 566 
 567         // Now to position the graphic and text. At this point I know the
 568         // contentX and contentY locations (including the padding and whatnot
 569         // that was defined on the Labeled). I also know the content width and
 570         // height. So now I just need to lay out the graphic and text within
 571         // that content x/y/w/h area.
 572         if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) {
 573             text.setManaged(true);
 574         }
 575 
 576         if (ignoreGraphic && ignoreText) {
 577             // There might be a text node as a child, or a graphic node as
 578             // a child. However we don't have to do anything for the graphic
 579             // node because the only way it can be a child and still have
 580             // ignoreGraphic true is if it is unmanaged. Text however might
 581             // be a child but still not matter, in which case we will just
 582             // stop managing it (although really I wish it just wasn't here
 583             // all all in that case)
 584             if (text.isManaged()) {
 585                 text.setManaged(false);
 586             }
 587             text.relocate(snapPosition(contentX), snapPosition(contentY));
 588         } else if (ignoreGraphic) {
 589             // Since I only have to position the text, it goes at the
 590             // contentX/contentY location. Note that positionNode will
 591             // adjust the text based on the text's minX/minY so no need to
 592             // worry about that here
 593             text.relocate(snapPosition(contentX), snapPosition(contentY));
 594             if (containsMnemonic) {
 595                 mnemonic_underscore.setEndX(mnemonicWidth-2.0);
 596                 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1);
 597             }
 598 
 599         } else if (ignoreText) {
 600             // there isn't text to display, so we need to position it
 601             // such that it doesn't affect the content area (although when
 602             // there is a graphic, the text isn't even in the scene)
 603             text.relocate(snapPosition(contentX), snapPosition(contentY));
 604             graphic.relocate(snapPosition(contentX), snapPosition(contentY));
 605             if (containsMnemonic) {
 606                 mnemonic_underscore.setEndX(mnemonicWidth);
 607                 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
 608                 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1);
 609 
 610             }
 611         } else {
 612             // There is both text and a graphic, so I need to position them
 613             // relative to each other
 614             double graphicX = 0;
 615             double graphicY = 0;
 616             double textX = 0;
 617             double textY = 0;
 618 
 619             if (contentDisplay == ContentDisplay.TOP) {
 620                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
 621                 textX = contentX + ((contentWidth - textWidth) / 2.0);
 622                 // The graphic is above the text, so it is positioned at
 623                 // graphicY and the text below it.
 624                 graphicY = contentY;
 625                 textY = graphicY + graphicHeight + gap;
 626             } else if (contentDisplay == ContentDisplay.RIGHT) {
 627                 // The graphic is to the right of the text
 628                 textX = contentX;
 629                 graphicX = textX + textWidth + gap;
 630                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
 631                 textY = contentY + ((contentHeight - textHeight) / 2.0);
 632             } else if (contentDisplay == ContentDisplay.BOTTOM) {
 633                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
 634                 textX = contentX + ((contentWidth - textWidth) / 2.0);
 635                 // The graphic is below the text
 636                 textY = contentY;
 637                 graphicY = textY + textHeight + gap;
 638             } else if (contentDisplay == ContentDisplay.LEFT) {
 639                 // The graphic is to the left of the text, so the graphicX is
 640                 // simply the contentX and the textX is to the right of it.
 641                 graphicX = contentX;
 642                 textX = graphicX + graphicWidth + gap;
 643                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
 644                 textY = contentY + ((contentHeight - textHeight) / 2.0);
 645             } else if (contentDisplay == ContentDisplay.CENTER) {
 646                 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0);
 647                 textX = contentX + ((contentWidth - textWidth) / 2.0);
 648                 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0);
 649                 textY = contentY + ((contentHeight - textHeight) / 2.0);
 650             }
 651             text.relocate(snapPosition(textX), snapPosition(textY));
 652             if (containsMnemonic) {
 653                 mnemonic_underscore.setEndX(mnemonicWidth);
 654                 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
 655                 mnemonic_underscore.relocate(snapPosition(textX+preMnemonicWidth), snapPosition(textY+mnemonicHeight-1));
 656             }
 657             graphic.relocate(snapPosition(graphicX), snapPosition(graphicY));
 658         }
 659 
 660         /**
 661          * check if the label text overflows it's bounds.
 662          * If there's an overflow, and no text clip then
 663          * we'll clip it.
 664          * If there is no overflow, and the label text has a
 665          * clip, then remove it.
 666          */
 667         if ((text != null) &&
 668                 ((text.getLayoutBounds().getHeight() > wrapHeight) ||
 669                         (text.getLayoutBounds().getWidth() > wrapWidth))) {
 670 
 671             if (textClip == null) {
 672                 textClip = new Rectangle();
 673             }
 674 
 675             if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) {
 676                 textClip.setX(text.getLayoutBounds().getMinX());
 677             } else {
 678                 textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth);
 679             }
 680             textClip.setY(text.getLayoutBounds().getMinY());
 681             textClip.setWidth(wrapWidth);
 682             textClip.setHeight(wrapHeight);
 683             if (text.getClip() == null) {
 684                 text.setClip(textClip);
 685             }
 686         }
 687         else {
 688             /**
 689              * content fits inside bounds, no need
 690              * for a clip
 691              */
 692             if (text.getClip() != null) {
 693                 text.setClip(null);
 694             }
 695         }
 696     }
 697 
 698     /** {@inheritDoc} */
 699     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 700         switch (attribute) {
 701             case TEXT: {
 702                 Labeled labeled = getSkinnable();
 703                 String accText = labeled.getAccessibleText();
 704                 if (accText != null && !accText.isEmpty()) return accText;
 705 
 706                 /* Use the text in the binding if available to handle mnemonics */
 707                 if (bindings != null) {
 708                     String text = bindings.getText();
 709                     if (text != null && !text.isEmpty()) return text;
 710                 }
 711                 /* Avoid the content in text.getText() as it can contain ellipses
 712                  * for clipping
 713                  */
 714                 if (labeled != null) {
 715                     String text = labeled.getText();
 716                     if (text != null && !text.isEmpty()) return text;
 717                 }
 718                 /* Use the graphic as last resource. Note that this implementation
 719                  * does not attempt to combine the label and graphics if both
 720                  * are being displayed
 721                  */
 722                 if (graphic != null) {
 723                     Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT);
 724                     if (result != null) return result;
 725                 }
 726                 return null;
 727             }
 728             case MNEMONIC: {
 729                 if (bindings != null) {
 730                     return bindings.getMnemonic();
 731                 }
 732                 return null;
 733             }
 734             default: return super.queryAccessibleAttribute(attribute, parameters);
 735         }
 736     }
 737 
 738 
 739 
 740     /***************************************************************************
 741      *                                                                         *
 742      * Private implementation                                                  *
 743      *                                                                         *
 744      **************************************************************************/
 745 
 746     private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 747         // First compute the minTextWidth by checking the width of the string
 748         // made by the ellipsis "...", and then by checking the width of the
 749         // string made up by labeled.text. We want the smaller of the two.
 750         final Labeled labeled = getSkinnable();
 751         final ContentDisplay contentDisplay = labeled.getContentDisplay();
 752         final double gap = labeled.getGraphicTextGap();
 753         double minTextWidth = 0;
 754 
 755         final Font font = text.getFont();
 756         OverrunStyle truncationStyle = labeled.getTextOverrun();
 757         String ellipsisString = labeled.getEllipsisString();
 758         final String string = labeled.getText();
 759         final boolean emptyText = string == null || string.isEmpty();
 760 
 761         if (!emptyText) {
 762             // We only want to recompute the full text width if the font or text changed
 763             if (truncationStyle == CLIP) {
 764                 if (textWidth == Double.NEGATIVE_INFINITY) {
 765                     // Show at minimum the first character
 766                     textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0);
 767                 }
 768                 minTextWidth = textWidth;
 769             } else {
 770                 if (textWidth == Double.NEGATIVE_INFINITY) {
 771                     textWidth = Utils.computeTextWidth(font, string, 0);
 772                 }
 773                 // We only want to recompute the ellipsis width if the font has changed
 774                 if (ellipsisWidth == Double.NEGATIVE_INFINITY) {
 775                     ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0);
 776                 }
 777                 minTextWidth = Math.min(textWidth, ellipsisWidth);
 778             }
 779         }
 780 
 781         // Now inspect the graphic and the hpos to determine the the minWidth
 782         final Node graphic = labeled.getGraphic();
 783         double width;
 784         if (isIgnoreGraphic()) {
 785             width = minTextWidth;
 786         } else if (isIgnoreText()) {
 787             width = graphic.minWidth(-1);
 788         } else if (contentDisplay == LEFT || contentDisplay == RIGHT){
 789             width = (minTextWidth + graphic.minWidth(-1) + gap);
 790         } else {
 791             width = Math.max(minTextWidth, graphic.minWidth(-1));
 792         }
 793 
 794         return width + leftInset + leftLabelPadding() +
 795                 rightInset + rightLabelPadding();
 796     }
 797 
 798     private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 799         final Labeled labeled = getSkinnable();
 800         final Font font = text.getFont();
 801 
 802         String str = labeled.getText();
 803         if (str != null && str.length() > 0) {
 804             int newlineIndex = str.indexOf('\n');
 805             if (newlineIndex >= 0) {
 806                 str = str.substring(0, newlineIndex);
 807             }
 808         }
 809 
 810         // TODO figure out how to cache this effectively.
 811         // Base minimum height on one line (ignoring wrapping here).
 812         double s = labeled.getLineSpacing();
 813         final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType());
 814 
 815         double h = textHeight;
 816 
 817         // Now we want to add on the graphic if necessary!
 818         if (!isIgnoreGraphic()) {
 819             final Node graphic = labeled.getGraphic();
 820             if (labeled.getContentDisplay() == ContentDisplay.TOP
 821                     || labeled.getContentDisplay() == ContentDisplay.BOTTOM) {
 822                 h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight;
 823             } else {
 824                 h = Math.max(textHeight, graphic.minHeight(width));
 825             }
 826         }
 827 
 828         return topInset + h + bottomInset + topLabelPadding() - bottomLabelPadding();
 829     }
 830 
 831     double topLabelPadding() {
 832         return snapSize(getSkinnable().getLabelPadding().getTop());
 833     }
 834 
 835     double bottomLabelPadding() {
 836         return snapSize(getSkinnable().getLabelPadding().getBottom());
 837     }
 838 
 839     double leftLabelPadding() {
 840         return snapSize(getSkinnable().getLabelPadding().getLeft());
 841     }
 842 
 843     double rightLabelPadding() {
 844         return snapSize(getSkinnable().getLabelPadding().getRight());
 845     }
 846 
 847 
 848     /**
 849      * Called whenever some state has changed that affects the text metrics.
 850      * Changes here will involve invalidating the display text so the next
 851      * call to updateDisplayedText computes a new value, and call requestLayout.
 852      */
 853     private void textMetricsChanged() {
 854         invalidText = true;
 855         getSkinnable().requestLayout();
 856     }
 857 
 858     /*
 859     ** The Label is a mnemonic, and it's target node
 860     ** has changed, but it's label hasn't so just
 861     ** swap them over, and tidy up.
 862     */
 863     void mnemonicTargetChanged() {
 864         if (containsMnemonic == true) {
 865             /*
 866             ** was there previously a labelFor  
 867             */
 868             removeMnemonic();
 869 
 870             /*
 871             ** is there a new labelFor
 872             */
 873             Control control = getSkinnable();
 874             if (control instanceof Label) {
 875                 labeledNode = ((Label)control).getLabelFor();
 876                 addMnemonic();
 877             }
 878             else {
 879                 labeledNode = null;
 880             }
 881         }
 882     }
 883 
 884     private void sceneChanged() {
 885         final Labeled labeled = getSkinnable();
 886         Scene scene = labeled.getScene();
 887 
 888         if (scene != null && containsMnemonic) {
 889             addMnemonic();
 890         }
 891 
 892     }
 893 
 894     /**
 895      * Marks minWidth as being invalid and in need of recomputation.
 896      */
 897     private void invalidateWidths() {
 898         textWidth = Double.NEGATIVE_INFINITY;
 899     }
 900 
 901     /**
 902      * Updates the content of the underlying Text node. This method should
 903      * only be called when necessary. If the invalidText flag is not set, then
 904      * the method is a no-op. This care is taken because recomputing the
 905      * text to display is an expensive operation. Package private ONLY FOR THE
 906      * SAKE OF TESTING.
 907      */
 908     void updateDisplayedText() {
 909         updateDisplayedText(-1, -1);
 910     }
 911 
 912     private void updateDisplayedText(double w, double h) {
 913         if (invalidText) {
 914             final Labeled labeled = getSkinnable();
 915             String s = labeled.getText();
 916 
 917             int mnemonicIndex = -1;
 918 
 919             /*
 920             ** if there's a valid string then parse it
 921             */
 922             if (s != null && s.length() > 0) {
 923                 bindings = new TextBinding(s);
 924 
 925                 if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) {
 926                     /*
 927                     ** the Labeled has a MnemonicParsing property,
 928                     ** if set true, then auto-parsing will check for
 929                     ** a mnemonic
 930                     */
 931                     if (labeled instanceof Label) {
 932                         // buttons etc
 933                         labeledNode = ((Label)labeled).getLabelFor();
 934                     } else {
 935                         labeledNode = labeled;
 936                     }
 937 
 938                     if (labeledNode == null) {
 939                         labeledNode = labeled;
 940                     }
 941                     mnemonicIndex = bindings.getMnemonicIndex() ;
 942                 }
 943             }
 944 
 945             /*
 946             ** we were previously a mnemonic
 947             */
 948             if (containsMnemonic) {
 949                 /*
 950                 ** are we no longer a mnemonic, or have we changed code?
 951                 */
 952                 if (mnemonicScene != null) {
 953                     if (mnemonicIndex == -1 ||
 954                             (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) {
 955                         removeMnemonic();
 956                     }
 957                     containsMnemonic = false;
 958                 }
 959             }
 960             else {
 961                 /*
 962                 ** this can happen if mnemonic parsing is
 963                 ** disabled on a previously valid mnemonic
 964                 */
 965                 removeMnemonic();
 966             }
 967 
 968             /*
 969             ** check we have a labeled
 970             */
 971             if (s != null && s.length() > 0) {
 972                 if (mnemonicIndex >= 0 && containsMnemonic == false) {
 973                     containsMnemonic = true;
 974                     mnemonicCode = bindings.getMnemonicKeyCombination();
 975                     addMnemonic();
 976                 }
 977             }
 978 
 979             if (containsMnemonic == true) {
 980                 s = bindings.getText();
 981                 if (mnemonic_underscore == null) {
 982                     mnemonic_underscore = new Line();
 983                     mnemonic_underscore.setStartX(0.0f);
 984                     mnemonic_underscore.setStartY(0.0f);
 985                     mnemonic_underscore.setEndY(0.0f);
 986                     mnemonic_underscore.getStyleClass().clear();
 987                     mnemonic_underscore.getStyleClass().setAll("mnemonic-underline");
 988                 }
 989                 if (!getChildren().contains(mnemonic_underscore)) {
 990                     getChildren().add(mnemonic_underscore);
 991                 }
 992             } else {
 993                 /*
 994                 ** we don't need a mnemonic....
 995                 */
 996                 if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) {
 997                     s = bindings.getText();
 998                 }
 999                 else {
1000                     s = labeled.getText();
1001                 }
1002                 if (mnemonic_underscore != null) {
1003                     if (getChildren().contains(mnemonic_underscore)) {
1004                         Platform.runLater(() -> {
1005                               getChildren().remove(mnemonic_underscore);
1006                               mnemonic_underscore = null;
1007                         });
1008                     }
1009                 }
1010             }
1011 
1012             int len = s != null ? s.length() : 0;
1013             boolean multiline = false;
1014 
1015             if (s != null && len > 0) {
1016                 int i = s.indexOf('\n');
1017                 if (i > -1 && i < len - 1) {
1018                     // Multiline text with embedded newlines - not
1019                     // taking into account a potential trailing newline.
1020                     multiline = true;
1021                 }
1022             }
1023 
1024             String result;
1025             boolean horizontalPosition =
1026                     (labeled.getContentDisplay() == ContentDisplay.LEFT ||
1027                     labeled.getContentDisplay() == ContentDisplay.RIGHT);
1028 
1029             double availableWidth = labeled.getWidth() - snappedLeftInset() - leftLabelPadding() -
1030                                     snappedRightInset() - rightLabelPadding();
1031             availableWidth = Math.max(availableWidth, 0);
1032 
1033             if (w == -1) {
1034                 w = availableWidth;
1035             }
1036             double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth);
1037             if (horizontalPosition && !isIgnoreGraphic()) {
1038                 double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap());
1039                 w -= graphicW;
1040                 minW -= graphicW;
1041             }
1042             wrapWidth = Math.max(minW, w);
1043 
1044             boolean verticalPosition =
1045                     (labeled.getContentDisplay() == ContentDisplay.TOP ||
1046                     labeled.getContentDisplay() == ContentDisplay.BOTTOM);
1047 
1048             double availableHeight = labeled.getHeight() - snappedTopInset() - topLabelPadding() -
1049                                      snappedBottomInset() - bottomLabelPadding();
1050             availableHeight = Math.max(availableHeight, 0);
1051 
1052             if (h == -1) {
1053                 h = availableHeight;
1054             }
1055             double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight);
1056             if (verticalPosition && labeled.getGraphic() != null) {
1057                 double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap();
1058                 h -= graphicH;
1059                 minH -= graphicH;
1060             }
1061             wrapHeight = Math.max(minH, h);
1062 
1063             updateWrappingWidth();
1064 
1065             Font font = text.getFont();
1066             OverrunStyle truncationStyle = labeled.getTextOverrun();
1067             String ellipsisString = labeled.getEllipsisString();
1068 
1069             if (labeled.isWrapText()) {
1070                 result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType());
1071             } else if (multiline) {
1072                 StringBuilder sb = new StringBuilder();
1073 
1074                 String[] splits = s.split("\n");
1075                 for (int i = 0; i < splits.length; i++) {
1076                     sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString));
1077                     if (i < splits.length - 1) {
1078                         sb.append('\n');
1079                     }
1080                 }
1081 
1082                 // TODO: Consider what to do in the case where vertical space is
1083                 // limited and the last visible line isn't already truncated
1084                 // with a trailing ellipsis. What if the style calls for leading
1085                 // or center ellipses? We could possibly add an additional
1086                 // trailing ellipsis to the last visible line, like this:
1087 
1088                 // +--------------------------------+
1089                 // |  This is some long text with multiple lines\n
1090                 // |  where more than one exceed the|width\n
1091                 // |  and wrapText is false, and all|lines\n
1092                 // +--don't fit.--------------------+
1093                 //
1094                 // +--------------------------------+
1095                 // |  This is some...multiple lines |
1096                 // |  where more t...ceed the width |
1097                 // |  and wrapText...d all lines... |
1098                 // +--------------------------------+
1099 
1100                 result = sb.toString();
1101             } else {
1102                 result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString);
1103             }
1104 
1105             if (result != null && result.endsWith("\n")) {
1106                 // Strip ending newline so we don't display another row.
1107                 result = result.substring(0, result.length() - 1);
1108             }
1109 
1110             text.setText(result);
1111             updateWrappingWidth();
1112             invalidText = false;
1113         }
1114     }
1115 
1116     private void addMnemonic() {
1117         if (labeledNode != null) {
1118             mnemonicScene = labeledNode.getScene();
1119             if (mnemonicScene != null) {
1120                 mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode));
1121             }
1122         }
1123     }
1124 
1125 
1126     private void removeMnemonic() {
1127         if (mnemonicScene != null && labeledNode != null) {
1128             mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode));
1129             mnemonicScene = null;
1130         }
1131     }
1132 
1133     /**
1134      * Updates the wrapping width of the text node. Although changing the font
1135      * does affect the metrics used for text layout, this method does not
1136      * call requestLayout or invalidate the text, since it may be called
1137      * from the constructor and such work would be duplicative and wasted.
1138      */
1139     private void updateWrappingWidth() {
1140         final Labeled labeled = getSkinnable();
1141         text.setWrappingWidth(0);
1142         if (labeled.isWrapText()) {
1143             // Note that the wrapping width needs to be set to zero before
1144             // getting the text's real preferred width.
1145             double w = Math.min(text.prefWidth(-1), wrapWidth);
1146             text.setWrappingWidth(w);
1147         }
1148     }
1149 
1150     /**
1151      * Gets whether for various computations we can ignore the presence of the graphic
1152      * (or lack thereof).
1153      * @return
1154      */
1155     boolean isIgnoreGraphic() {
1156         return (graphic == null ||
1157                 !graphic.isManaged() ||
1158                 getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY);
1159     }
1160 
1161     /**
1162      * Gets whether for various computations we can ignore the presence of the text.
1163      * @return
1164      */
1165     boolean isIgnoreText() {
1166         final Labeled labeled = getSkinnable();
1167         final String txt = labeled.getText();
1168         return (txt == null ||
1169                 txt.equals("") ||
1170                 labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY);
1171     }
1172 }