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.text;
  27 
  28 import javafx.css.converter.BooleanConverter;
  29 import javafx.css.converter.EnumConverter;
  30 import javafx.css.converter.SizeConverter;
  31 import com.sun.javafx.geom.BaseBounds;
  32 import com.sun.javafx.geom.Path2D;
  33 import com.sun.javafx.geom.RectBounds;
  34 import com.sun.javafx.geom.TransformedShape;
  35 import com.sun.javafx.geom.transform.BaseTransform;
  36 import com.sun.javafx.scene.DirtyBits;
  37 import com.sun.javafx.scene.text.GlyphList;
  38 import com.sun.javafx.scene.text.TextLayout;
  39 import com.sun.javafx.scene.text.TextLayoutFactory;
  40 import com.sun.javafx.scene.text.TextLine;
  41 import com.sun.javafx.scene.text.TextSpan;
  42 import com.sun.javafx.sg.prism.NGNode;
  43 import com.sun.javafx.sg.prism.NGShape;
  44 import com.sun.javafx.sg.prism.NGText;
  45 import com.sun.javafx.tk.Toolkit;
  46 import javafx.beans.DefaultProperty;
  47 import javafx.beans.InvalidationListener;
  48 import javafx.beans.binding.DoubleBinding;
  49 import javafx.beans.binding.ObjectBinding;
  50 import javafx.beans.property.*;
  51 import javafx.css.*;
  52 import javafx.geometry.*;
  53 import javafx.scene.AccessibleAttribute;
  54 import javafx.scene.AccessibleRole;
  55 import javafx.scene.paint.Color;
  56 import javafx.scene.paint.Paint;
  57 import javafx.scene.shape.LineTo;
  58 import javafx.scene.shape.MoveTo;
  59 import javafx.scene.shape.PathElement;
  60 import javafx.scene.shape.Shape;
  61 import javafx.scene.shape.StrokeType;
  62 import java.util.ArrayList;
  63 import java.util.Collections;
  64 import java.util.List;
  65 
  66 /**
  67  * The {@code Text} class defines a node that displays a text.
  68  *
  69  * Paragraphs are separated by {@code '\n'} and the text is wrapped on
  70  * paragraph boundaries.
  71  *
  72 <PRE>
  73 import javafx.scene.text.*;
  74 
  75 Text t = new Text(10, 50, "This is a test");
  76 t.setFont(new Font(20));
  77 </PRE>
  78  *
  79 <PRE>
  80 import javafx.scene.text.*;
  81 
  82 Text t = new Text();
  83 text.setFont(new Font(20));
  84 text.setText("First row\nSecond row");
  85 </PRE>
  86  *
  87 <PRE>
  88 import javafx.scene.text.*;
  89 
  90 Text t = new Text();
  91 text.setFont(new Font(20));
  92 text.setWrappingWidth(200);
  93 text.setTextAlignment(TextAlignment.JUSTIFY)
  94 text.setText("The quick brown fox jumps over the lazy dog");
  95 </PRE>
  96  * @since JavaFX 2.0
  97  */
  98 @DefaultProperty("text")
  99 public class Text extends Shape {
 100 
 101     private TextLayout layout;
 102     private static final PathElement[] EMPTY_PATH_ELEMENT_ARRAY = new PathElement[0];
 103 
 104     /**
 105      * @treatAsPrivate implementation detail
 106      * @deprecated This is an internal API that is not intended
 107      * for use and will be removed in the next version
 108      */
 109     @Deprecated
 110     @Override
 111     protected final NGNode impl_createPeer() {
 112         return new NGText();
 113     }
 114 
 115     /**
 116      * Creates an empty instance of Text.
 117      */
 118     public Text() {
 119         setAccessibleRole(AccessibleRole.TEXT);
 120         InvalidationListener listener = observable -> checkSpan();
 121         parentProperty().addListener(listener);
 122         managedProperty().addListener(listener);
 123         effectiveNodeOrientationProperty().addListener(observable -> checkOrientation());
 124         setPickOnBounds(true);
 125     }
 126 
 127     /**
 128      * Creates an instance of Text containing the given string.
 129      * @param text text to be contained in the instance
 130      */
 131     public Text(String text) {
 132         this();
 133         setText(text);
 134     }
 135 
 136     /**
 137      * Creates an instance of Text on the given coordinates containing the
 138      * given string.
 139      * @param x the horizontal position of the text
 140      * @param y the vertical position of the text
 141      * @param text text to be contained in the instance
 142      */
 143     public Text(double x, double y, String text) {
 144         this(text);
 145         setX(x);
 146         setY(y);
 147     }
 148 
 149     private boolean isSpan;
 150     private boolean isSpan() {
 151         return isSpan;
 152     }
 153 
 154     private void checkSpan() {
 155         isSpan = isManaged() && getParent() instanceof TextFlow;
 156         if (isSpan() && !pickOnBoundsProperty().isBound()) {
 157             /* Documented behavior. See class description for TextFlow */
 158             setPickOnBounds(false);
 159         }
 160     }
 161 
 162     private void checkOrientation() {
 163         if (!isSpan()) {
 164             NodeOrientation orientation = getEffectiveNodeOrientation();
 165             boolean rtl =  orientation == NodeOrientation.RIGHT_TO_LEFT;
 166             int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR;
 167             TextLayout layout = getTextLayout();
 168             if (layout.setDirection(dir)) {
 169                 needsTextLayout();
 170             }
 171         }
 172     }
 173 
 174     @Override
 175     public boolean usesMirroring() {
 176         return false;
 177     }
 178 
 179     private void needsFullTextLayout() {
 180         if (isSpan()) {
 181             /* Create new text span every time the font or text changes
 182              * so the text layout can see that the content has changed.
 183              */
 184             textSpan = null;
 185 
 186             /* Relies on impl_geomChanged() to request text flow to relayout */
 187         } else {
 188             TextLayout layout = getTextLayout();
 189             String string = getTextInternal();
 190             Object font = getFontInternal();
 191             layout.setContent(string, font);
 192         }
 193         needsTextLayout();
 194     }
 195 
 196     private void needsTextLayout() {
 197         textRuns = null;
 198         impl_geomChanged();
 199         impl_markDirty(DirtyBits.NODE_CONTENTS);
 200     }
 201 
 202     private TextSpan textSpan;
 203     TextSpan getTextSpan() {
 204         if (textSpan == null) {
 205             textSpan = new TextSpan() {
 206                 @Override public String getText() {
 207                     return getTextInternal();
 208                 }
 209                 @Override public Object getFont() {
 210                     return getFontInternal();
 211                 }
 212                 @Override public RectBounds getBounds() {
 213                     return null;
 214                 }
 215             };
 216         }
 217         return textSpan;
 218     }
 219 
 220     private TextLayout getTextLayout() {
 221         if (isSpan()) {
 222             layout = null;
 223             TextFlow parent = (TextFlow)getParent();
 224             return parent.getTextLayout();
 225         }
 226         if (layout == null) {
 227             TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory();
 228             layout = factory.createLayout();
 229             String string = getTextInternal();
 230             Object font = getFontInternal();
 231             TextAlignment alignment = getTextAlignment();
 232             if (alignment == null) alignment = DEFAULT_TEXT_ALIGNMENT;
 233             layout.setContent(string, font);
 234             layout.setAlignment(alignment.ordinal());
 235             layout.setLineSpacing((float)getLineSpacing());
 236             layout.setWrapWidth((float)getWrappingWidth());
 237             if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
 238                 layout.setDirection(TextLayout.DIRECTION_RTL);
 239             } else {
 240                 layout.setDirection(TextLayout.DIRECTION_LTR);
 241             }
 242         }
 243         return layout;
 244     }
 245 
 246     private GlyphList[] textRuns = null;
 247     private BaseBounds spanBounds = new RectBounds(); /* relative to the textlayout */
 248     private boolean spanBoundsInvalid = true;
 249 
 250     void layoutSpan(GlyphList[] runs) {
 251         TextSpan span = getTextSpan();
 252         int count = 0;
 253         for (int i = 0; i < runs.length; i++) {
 254             GlyphList run = runs[i];
 255             if (run.getTextSpan() == span) {
 256                 count++;
 257             }
 258         }
 259         textRuns = new GlyphList[count];
 260         count = 0;
 261         for (int i = 0; i < runs.length; i++) {
 262             GlyphList run = runs[i];
 263             if (run.getTextSpan() == span) {
 264                 textRuns[count++] = run;
 265             }
 266         }
 267         spanBoundsInvalid = true;
 268 
 269         /* Sometimes a property change in the text node will causes layout in
 270          * text flow. In this case all the dirty bits are already clear and no
 271          * extra work is necessary. Other times the layout is caused by changes
 272          * in the text flow object (wrapping width and text alignment for example).
 273          * In the second case the dirty bits must be set here using
 274          * impl_geomChanged() and impl_markDirty(). Note that impl_geomChanged()
 275          * causes another (undesired) layout request in the parent.
 276          * In general this is not a problem because shapes are not resizable and
 277          * region objects do not propagate layout changes to the parent.
 278          * This is a special case where a shape is resized by the parent during
 279          * layoutChildren(). See TextFlow#requestLayout() for information how
 280          * text flow deals with this situation.
 281          */
 282         impl_geomChanged();
 283         impl_markDirty(DirtyBits.NODE_CONTENTS);
 284     }
 285 
 286     BaseBounds getSpanBounds() {
 287         if (spanBoundsInvalid) {
 288             GlyphList[] runs = getRuns();
 289             if (runs.length != 0) {
 290                 float left = Float.POSITIVE_INFINITY;
 291                 float top = Float.POSITIVE_INFINITY;
 292                 float right = 0;
 293                 float bottom = 0;
 294                 for (int i = 0; i < runs.length; i++) {
 295                     GlyphList run = runs[i];
 296                     com.sun.javafx.geom.Point2D location = run.getLocation();
 297                     float width = run.getWidth();
 298                     float height = run.getLineBounds().getHeight();
 299                     left = Math.min(location.x, left);
 300                     top = Math.min(location.y, top);
 301                     right = Math.max(location.x + width, right);
 302                     bottom = Math.max(location.y + height, bottom);
 303                 }
 304                 spanBounds = spanBounds.deriveWithNewBounds(left, top, 0,
 305                                                             right, bottom, 0);
 306             } else {
 307                 spanBounds = spanBounds.makeEmpty();
 308             }
 309             spanBoundsInvalid = false;
 310         }
 311         return spanBounds;
 312     }
 313 
 314     private GlyphList[] getRuns() {
 315         if (textRuns != null) return textRuns;
 316         if (isSpan()) {
 317             /* List of run is initialized when the TextFlow layout the children */
 318             getParent().layout();
 319         } else {
 320             TextLayout layout = getTextLayout();
 321             textRuns = layout.getRuns();
 322         }
 323         return textRuns;
 324     }
 325 
 326     private com.sun.javafx.geom.Shape getShape() {
 327         TextLayout layout = getTextLayout();
 328         /* TextLayout has the text shape cached */
 329         int type = TextLayout.TYPE_TEXT;
 330         if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH;
 331         if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE;
 332 
 333         TextSpan filter = null;
 334         if (isSpan()) {
 335             /* Spans are always relative to the top */
 336             type |= TextLayout.TYPE_TOP;
 337             filter = getTextSpan();
 338         } else {
 339             /* Relative to baseline (first line)
 340              * This shape can be translate in the y axis according
 341              * to text origin, see impl_configShape().
 342              */
 343             type |= TextLayout.TYPE_BASELINE;
 344         }
 345         return layout.getShape(type, filter);
 346     }
 347 
 348     private BaseBounds getVisualBounds() {
 349         if (impl_mode == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) {
 350             int type = TextLayout.TYPE_TEXT;
 351             if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH;
 352             if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE;
 353             return getTextLayout().getVisualBounds(type);
 354         } else {
 355             return getShape().getBounds();
 356         }
 357     }
 358 
 359     private BaseBounds getLogicalBounds() {
 360         TextLayout layout = getTextLayout();
 361         /* TextLayout has the bounds cached */
 362         return layout.getBounds();
 363     }
 364 
 365     /**
 366      * Defines text string that is to be displayed.
 367      *
 368      * @defaultValue empty string
 369      */
 370     private StringProperty text;
 371 
 372     public final void setText(String value) {
 373         if (value == null) value = "";
 374         textProperty().set(value);
 375     }
 376 
 377     public final String getText() {
 378         return text == null ? "" : text.get();
 379     }
 380 
 381     private String getTextInternal() {
 382         // this might return null in case of bound property
 383         String localText = getText();
 384         return localText == null ? "" : localText;
 385     }
 386 
 387     public final StringProperty textProperty() {
 388         if (text == null) {
 389             text = new StringPropertyBase("") {
 390                 @Override public Object getBean() { return Text.this; }
 391                 @Override public String getName() { return "text"; }
 392                 @Override  public void invalidated() {
 393                     needsFullTextLayout();
 394                     setSelectionStart(-1);
 395                     setSelectionEnd(-1);
 396                     setCaretPosition(-1);
 397                     setCaretBias(true);
 398 
 399                     // MH: Functionality copied from store() method,
 400                     // which was removed.
 401                     // Wonder what should happen if text is bound
 402                     //  and becomes null?
 403                     final String value = get();
 404                     if ((value == null) && !isBound()) {
 405                         set("");
 406                     }
 407                     notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
 408                 }
 409             };
 410         }
 411         return text;
 412     }
 413 
 414     /**
 415      * Defines the X coordinate of text origin.
 416      *
 417      * @defaultValue 0
 418      */
 419     private DoubleProperty x;
 420 
 421     public final void setX(double value) {
 422         xProperty().set(value);
 423     }
 424 
 425     public final double getX() {
 426         return x == null ? 0.0 : x.get();
 427     }
 428 
 429     public final DoubleProperty xProperty() {
 430         if (x == null) {
 431             x = new DoublePropertyBase() {
 432                 @Override public Object getBean() { return Text.this; }
 433                 @Override public String getName() { return "x"; }
 434                 @Override public void invalidated() {
 435                     impl_geomChanged();
 436                 }
 437             };
 438         }
 439         return x;
 440     }
 441 
 442     /**
 443      * Defines the Y coordinate of text origin.
 444      *
 445      * @defaultValue 0
 446      */
 447     private DoubleProperty y;
 448 
 449     public final void setY(double value) {
 450         yProperty().set(value);
 451     }
 452 
 453     public final double getY() {
 454         return y == null ? 0.0 : y.get();
 455     }
 456 
 457     public final DoubleProperty yProperty() {
 458         if (y == null) {
 459             y = new DoublePropertyBase() {
 460                 @Override public Object getBean() { return Text.this; }
 461                 @Override public String getName() { return "y"; }
 462                 @Override public void invalidated() {
 463                     impl_geomChanged();
 464                 }
 465             };
 466         }
 467         return y;
 468     }
 469 
 470     /**
 471      * Defines the font of text.
 472      *
 473      * @defaultValue Font{}
 474      */
 475     private ObjectProperty<Font> font;
 476 
 477     public final void setFont(Font value) {
 478         fontProperty().set(value);
 479     }
 480 
 481     public final Font getFont() {
 482         return font == null ? Font.getDefault() : font.get();
 483     }
 484 
 485     /**
 486      * Internally used safe version of getFont which never returns null.
 487      *
 488      * @return the font
 489      */
 490     private Object getFontInternal() {
 491         Font font = getFont();
 492         if (font == null) font = Font.getDefault();
 493         return font.impl_getNativeFont();
 494     }
 495 
 496     public final ObjectProperty<Font> fontProperty() {
 497         if (font == null) {
 498             font = new StyleableObjectProperty<Font>(Font.getDefault()) {
 499                 @Override public Object getBean() { return Text.this; }
 500                 @Override public String getName() { return "font"; }
 501                 @Override public CssMetaData<Text,Font> getCssMetaData() {
 502                     return StyleableProperties.FONT;
 503                 }
 504                 @Override public void invalidated() {
 505                     needsFullTextLayout();
 506                     impl_markDirty(DirtyBits.TEXT_FONT);
 507                 }
 508             };
 509         }
 510         return font;
 511     }
 512 
 513     public final void setTextOrigin(VPos value) {
 514         textOriginProperty().set(value);
 515     }
 516 
 517     public final VPos getTextOrigin() {
 518         if (attributes == null || attributes.textOrigin == null) {
 519             return DEFAULT_TEXT_ORIGIN;
 520         }
 521         return attributes.getTextOrigin();
 522     }
 523 
 524     /**
 525      * Defines the origin of text coordinate system in local coordinates.
 526      * Note: in case multiple rows are rendered {@code VPos.BASELINE} and
 527      * {@code VPos.TOP} define the origin of the top row while
 528      * {@code VPos.BOTTOM} defines the origin of the bottom row.
 529      *
 530      * @defaultValue VPos.BASELINE
 531      */
 532     public final ObjectProperty<VPos> textOriginProperty() {
 533         return getTextAttribute().textOriginProperty();
 534     }
 535 
 536     /**
 537      * Determines how the bounds of the text node are calculated.
 538      * Logical bounds is a more appropriate default for text than
 539      * the visual bounds. See {@code TextBoundsType} for more information.
 540      *
 541      * @defaultValue TextBoundsType.LOGICAL
 542      */
 543     private ObjectProperty<TextBoundsType> boundsType;
 544 
 545     public final void setBoundsType(TextBoundsType value) {
 546         boundsTypeProperty().set(value);
 547     }
 548 
 549     public final TextBoundsType getBoundsType() {
 550         return boundsType == null ?
 551             DEFAULT_BOUNDS_TYPE : boundsTypeProperty().get();
 552     }
 553 
 554     public final ObjectProperty<TextBoundsType> boundsTypeProperty() {
 555         if (boundsType == null) {
 556             boundsType =
 557                new StyleableObjectProperty<TextBoundsType>(DEFAULT_BOUNDS_TYPE) {
 558                    @Override public Object getBean() { return Text.this; }
 559                    @Override public String getName() { return "boundsType"; }
 560                    @Override public CssMetaData<Text,TextBoundsType> getCssMetaData() {
 561                        return StyleableProperties.BOUNDS_TYPE;
 562                    }
 563                    @Override public void invalidated() {
 564                        TextLayout layout = getTextLayout();
 565                        int type = 0;
 566                        if (boundsType.get() == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 567                            type |= TextLayout.BOUNDS_CENTER;
 568                        }
 569                        if (layout.setBoundsType(type)) {
 570                            needsTextLayout();
 571                        } else {
 572                            impl_geomChanged();
 573                        }
 574                    }
 575             };
 576         }
 577         return boundsType;
 578     }
 579 
 580     /**
 581      * Defines a width constraint for the text in user space coordinates,
 582      * e.g. pixels, not glyph or character count.
 583      * If the value is {@code > 0} text will be line wrapped as needed
 584      * to satisfy this constraint.
 585      *
 586      * @defaultValue 0
 587      */
 588     private DoubleProperty wrappingWidth;
 589 
 590     public final void setWrappingWidth(double value) {
 591         wrappingWidthProperty().set(value);
 592     }
 593 
 594     public final double getWrappingWidth() {
 595         return wrappingWidth == null ? 0 : wrappingWidth.get();
 596     }
 597 
 598     public final DoubleProperty wrappingWidthProperty() {
 599         if (wrappingWidth == null) {
 600             wrappingWidth = new DoublePropertyBase() {
 601                 @Override public Object getBean() { return Text.this; }
 602                 @Override public String getName() { return "wrappingWidth"; }
 603                 @Override public void invalidated() {
 604                     if (!isSpan()) {
 605                         TextLayout layout = getTextLayout();
 606                         if (layout.setWrapWidth((float)get())) {
 607                             needsTextLayout();
 608                         } else {
 609                             impl_geomChanged();
 610                         }
 611                     }
 612                 }
 613             };
 614         }
 615         return wrappingWidth;
 616     }
 617 
 618     public final void setUnderline(boolean value) {
 619         underlineProperty().set(value);
 620     }
 621 
 622     public final boolean isUnderline() {
 623         if (attributes == null || attributes.underline == null) {
 624             return DEFAULT_UNDERLINE;
 625         }
 626         return attributes.isUnderline();
 627     }
 628 
 629     /**
 630      * Defines if each line of text should have a line below it.
 631      *
 632      * @defaultValue false
 633      */
 634     public final BooleanProperty underlineProperty() {
 635         return getTextAttribute().underlineProperty();
 636     }
 637 
 638     public final void setStrikethrough(boolean value) {
 639         strikethroughProperty().set(value);
 640     }
 641 
 642     public final boolean isStrikethrough() {
 643         if (attributes == null || attributes.strikethrough == null) {
 644             return DEFAULT_STRIKETHROUGH;
 645         }
 646         return attributes.isStrikethrough();
 647     }
 648 
 649     /**
 650      * Defines if each line of text should have a line through it.
 651      *
 652      * @defaultValue false
 653      */
 654     public final BooleanProperty strikethroughProperty() {
 655         return getTextAttribute().strikethroughProperty();
 656     }
 657 
 658     public final void setTextAlignment(TextAlignment value) {
 659         textAlignmentProperty().set(value);
 660     }
 661 
 662     public final TextAlignment getTextAlignment() {
 663         if (attributes == null || attributes.textAlignment == null) {
 664             return DEFAULT_TEXT_ALIGNMENT;
 665         }
 666         return attributes.getTextAlignment();
 667     }
 668 
 669     /**
 670      * Defines horizontal text alignment in the bounding box.
 671      *
 672      * The width of the bounding box is defined by the widest row.
 673      *
 674      * Note: In the case of a single line of text, where the width of the
 675      * node is determined by the width of the text, the alignment setting
 676      * has no effect.
 677      *
 678      * @defaultValue TextAlignment.LEFT
 679      */
 680     public final ObjectProperty<TextAlignment> textAlignmentProperty() {
 681         return getTextAttribute().textAlignmentProperty();
 682     }
 683 
 684     public final void setLineSpacing(double spacing) {
 685         lineSpacingProperty().set(spacing);
 686     }
 687 
 688     public final double getLineSpacing() {
 689         if (attributes == null || attributes.lineSpacing == null) {
 690             return DEFAULT_LINE_SPACING;
 691         }
 692         return attributes.getLineSpacing();
 693     }
 694 
 695     /**
 696      * Defines the vertical space in pixel between lines.
 697      *
 698      * @defaultValue 0
 699      *
 700      * @since JavaFX 8.0
 701      */
 702     public final DoubleProperty lineSpacingProperty() {
 703         return getTextAttribute().lineSpacingProperty();
 704     }
 705 
 706     @Override
 707     public final double getBaselineOffset() {
 708         return baselineOffsetProperty().get();
 709     }
 710 
 711     /**
 712      * The 'alphabetic' (or roman) baseline offset from the Text node's
 713      * layoutBounds.minY location.
 714      * The value typically corresponds to the max ascent of the font.
 715      */
 716     public final ReadOnlyDoubleProperty baselineOffsetProperty() {
 717         return getTextAttribute().baselineOffsetProperty();
 718     }
 719 
 720     /**
 721      * Specifies a requested font smoothing type : gray or LCD.
 722      *
 723      * The width of the bounding box is defined by the widest row.
 724      *
 725      * Note: LCD mode doesn't apply in numerous cases, such as various
 726      * compositing modes, where effects are applied and very large glyphs.
 727      *
 728      * @defaultValue FontSmoothingType.GRAY
 729      * @since JavaFX 2.1
 730      */
 731     private ObjectProperty<FontSmoothingType> fontSmoothingType;
 732 
 733     public final void setFontSmoothingType(FontSmoothingType value) {
 734         fontSmoothingTypeProperty().set(value);
 735     }
 736 
 737     public final FontSmoothingType getFontSmoothingType() {
 738         return fontSmoothingType == null ?
 739             FontSmoothingType.GRAY : fontSmoothingType.get();
 740     }
 741 
 742     public final ObjectProperty<FontSmoothingType>
 743         fontSmoothingTypeProperty() {
 744         if (fontSmoothingType == null) {
 745             fontSmoothingType =
 746                 new StyleableObjectProperty<FontSmoothingType>
 747                                                (FontSmoothingType.GRAY) {
 748                 @Override public Object getBean() { return Text.this; }
 749                 @Override public String getName() { return "fontSmoothingType"; }
 750                 @Override public CssMetaData<Text,FontSmoothingType> getCssMetaData() {
 751                     return StyleableProperties.FONT_SMOOTHING_TYPE;
 752                 }
 753                 @Override public void invalidated() {
 754                     impl_markDirty(DirtyBits.TEXT_ATTRS);
 755                     impl_geomChanged();
 756                 }
 757             };
 758         }
 759         return fontSmoothingType;
 760     }
 761 
 762     /**
 763      * @treatAsPrivate implementation detail
 764      * @deprecated This is an internal API that is not intended
 765      * for use and will be removed in the next version
 766      */
 767     @Deprecated
 768     @Override
 769     protected final void impl_geomChanged() {
 770         super.impl_geomChanged();
 771         if (attributes != null) {
 772             if (attributes.impl_caretBinding != null) {
 773                 attributes.impl_caretBinding.invalidate();
 774             }
 775             if (attributes.impl_selectionBinding != null) {
 776                 attributes.impl_selectionBinding.invalidate();
 777             }
 778         }
 779         impl_markDirty(DirtyBits.NODE_GEOMETRY);
 780     }
 781 
 782     /**
 783      * Shape of selection in local coordinates.
 784      *
 785      * @since 9
 786      */
 787     public final PathElement[] getSelectionShape() {
 788         return selectionShapeProperty().get();
 789     }
 790 
 791     public final ReadOnlyObjectProperty<PathElement[]> selectionShapeProperty() {
 792         return getTextAttribute().impl_selectionShapeProperty();
 793     }
 794 
 795     /**
 796      * Selection start index in the content.
 797      * set to {@code -1} to unset selection.
 798      *
 799      * @since 9
 800      */
 801     public final void setSelectionStart(int value) {
 802         if (value == -1 &&
 803                 (attributes == null || attributes.impl_selectionStart == null)) {
 804             return;
 805         }
 806         selectionStartProperty().set(value);
 807     }
 808 
 809     public final int getSelectionStart() {
 810         if (attributes == null || attributes.impl_selectionStart == null) {
 811             return DEFAULT_SELECTION_START;
 812         }
 813         return attributes.getImpl_selectionStart();
 814     }
 815 
 816     public final IntegerProperty selectionStartProperty() {
 817         return getTextAttribute().impl_selectionStartProperty();
 818     }
 819 
 820     /**
 821      * Selection end index in the content.
 822      * set to {@code -1} to unset selection.
 823      *
 824      * @since 9
 825      */
 826     public final void setSelectionEnd(int value) {
 827         if (value == -1 &&
 828                 (attributes == null || attributes.impl_selectionEnd == null)) {
 829             return;
 830         }
 831         selectionEndProperty().set(value);
 832     }
 833 
 834     public final int getSelectionEnd() {
 835         if (attributes == null || attributes.impl_selectionEnd == null) {
 836             return DEFAULT_SELECTION_END;
 837         }
 838         return attributes.getImpl_selectionEnd();
 839     }
 840 
 841     public final IntegerProperty selectionEndProperty() {
 842         return getTextAttribute().impl_selectionEndProperty();
 843     }
 844 
 845     /**
 846      * The fill color of selected text.
 847      *
 848      * @since 9
 849      */
 850     public final ObjectProperty<Paint> selectionFillProperty() {
 851         return getTextAttribute().impl_selectionFillProperty();
 852     }
 853 
 854     public final void setSelectionFill(Paint paint) {
 855         selectionFillProperty().set(paint);
 856     }
 857     public final Paint getSelectionFill() {
 858         return selectionFillProperty().get();
 859     }
 860 
 861     /**
 862      * Shape of caret in local coordinates.
 863      *
 864      * @since 9
 865      */
 866     public final PathElement[] getCaretShape() {
 867         return caretShapeProperty().get();
 868     }
 869 
 870     public final ReadOnlyObjectProperty<PathElement[]> caretShapeProperty() {
 871         return getTextAttribute().impl_caretShapeProperty();
 872     }
 873 
 874     /**
 875      * caret index in the content.
 876      * set to {@code -1} to unset caret.
 877      *
 878      * @since 9
 879      */
 880     public final void setCaretPosition(int value) {
 881         if (value == -1 &&
 882                 (attributes == null || attributes.impl_caretPosition == null)) {
 883             return;
 884         }
 885         caretPositionProperty().set(value);
 886     }
 887 
 888     public final int getCaretPosition() {
 889         if (attributes == null || attributes.impl_caretPosition == null) {
 890             return DEFAULT_CARET_POSITION;
 891         }
 892         return attributes.getImpl_caretPosition();
 893     }
 894 
 895     public final IntegerProperty caretPositionProperty() {
 896         return getTextAttribute().impl_caretPositionProperty();
 897     }
 898 
 899     /**
 900      * caret bias in the content. {@code true} means a bias towards the leading character edge.
 901      * (true=leading/false=trailing)
 902      *
 903      * @since 9
 904      */
 905     public final void setCaretBias(boolean value) {
 906         if (value && (attributes == null || attributes.impl_caretBias == null)) {
 907             return;
 908         }
 909         caretBiasProperty().set(value);
 910     }
 911 
 912     public final boolean isCaretBias() {
 913         if (attributes == null || attributes.impl_caretBias == null) {
 914             return DEFAULT_CARET_BIAS;
 915         }
 916         return getTextAttribute().isImpl_caretBias();
 917     }
 918 
 919     public final BooleanProperty caretBiasProperty() {
 920         return getTextAttribute().impl_caretBiasProperty();
 921     }
 922 
 923     /**
 924      * Maps local point to index in the content.
 925      *
 926      * @since 9
 927      */
 928     public final HitInfo hitTest(Point2D point) {
 929         if (point == null) return null;
 930         TextLayout layout = getTextLayout();
 931         double x = point.getX() - getX();
 932         double y = point.getY() - getY() + getYRendering();
 933         TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y);
 934         return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(),
 935                            layoutHit.isLeading(), getText());
 936     }
 937 
 938     private PathElement[] getRange(int start, int end, int type) {
 939         int length = getTextInternal().length();
 940         if (0 <= start && start < end  && end <= length) {
 941             TextLayout layout = getTextLayout();
 942             float x = (float)getX();
 943             float y = (float)getY() - getYRendering();
 944             return layout.getRange(start, end, type, x, y);
 945         }
 946         return EMPTY_PATH_ELEMENT_ARRAY;
 947     }
 948 
 949     /**
 950      * Returns shape for the caret at given index and bias.
 951      *
 952      * @since 9
 953      */
 954     public final PathElement[] caretShape(int charIndex, boolean caretBias) {
 955         if (0 <= charIndex && charIndex <= getTextInternal().length()) {
 956             float x = (float)getX();
 957             float y = (float)getY() - getYRendering();
 958             return getTextLayout().getCaretShape(charIndex, caretBias, x, y);
 959         } else {
 960             return null;
 961         }
 962     }
 963 
 964     /**
 965      * Returns shape for the range of the text in local coordinates.
 966      *
 967      * @since 9
 968      */
 969     public final PathElement[] rangeShape(int start, int end) {
 970         return getRange(start, end, TextLayout.TYPE_TEXT);
 971     }
 972 
 973     /**
 974      * Returns shape for the underline in local coordinates.
 975      *
 976      * @since 9
 977      */
 978     public final PathElement[] underlineShape(int start, int end) {
 979         return getRange(start, end, TextLayout.TYPE_UNDERLINE);
 980     }
 981 
 982     /**
 983      * Shows/Hides on-screen keyboard if available (mobile platform)
 984      *
 985      * @treatAsPrivate implementation detail
 986      * @deprecated This is an internal API that is not intended
 987      * for use and will be removed in the next version
 988      */
 989     @Deprecated
 990     public final void impl_displaySoftwareKeyboard(boolean display) {
 991     }
 992 
 993     private float getYAdjustment(BaseBounds bounds) {
 994         VPos origin = getTextOrigin();
 995         if (origin == null) origin = DEFAULT_TEXT_ORIGIN;
 996         switch (origin) {
 997         case TOP: return -bounds.getMinY();
 998         case BASELINE: return 0;
 999         case CENTER: return -bounds.getMinY() - bounds.getHeight() / 2;
1000         case BOTTOM: return -bounds.getMinY() - bounds.getHeight();
1001         default: return 0;
1002         }
1003     }
1004 
1005     private float getYRendering() {
1006         if (isSpan()) return 0;
1007 
1008         /* Always logical for rendering */
1009         BaseBounds bounds = getLogicalBounds();
1010 
1011         VPos origin = getTextOrigin();
1012         if (origin == null) origin = DEFAULT_TEXT_ORIGIN;
1013         if (getBoundsType() == TextBoundsType.VISUAL) {
1014             BaseBounds vBounds = getVisualBounds();
1015             float delta = vBounds.getMinY() - bounds.getMinY();
1016             switch (origin) {
1017             case TOP: return delta;
1018             case BASELINE: return -vBounds.getMinY() + delta;
1019             case CENTER: return vBounds.getHeight() / 2 + delta;
1020             case BOTTOM: return vBounds.getHeight() + delta;
1021             default: return 0;
1022             }
1023         } else {
1024             switch (origin) {
1025             case TOP: return 0;
1026             case BASELINE: return -bounds.getMinY();
1027             case CENTER: return bounds.getHeight() / 2;
1028             case BOTTOM: return bounds.getHeight();
1029             default: return 0;
1030             }
1031         }
1032     }
1033 
1034     /**
1035      * @treatAsPrivate implementation detail
1036      * @deprecated This is an internal API that is not intended
1037      * for use and will be removed in the next version
1038      */
1039     @Deprecated
1040     @Override
1041     protected final Bounds impl_computeLayoutBounds() {
1042         if (isSpan()) {
1043             BaseBounds bounds = getSpanBounds();
1044             double width = bounds.getWidth();
1045             double height = bounds.getHeight();
1046             return new BoundingBox(0, 0, width, height);
1047         }
1048 
1049         if (getBoundsType() == TextBoundsType.VISUAL) {
1050             /* In Node the layout bounds is computed based in the geom
1051              * bounds and in Shape the geom bounds is computed based
1052              * on the shape (generated here in #configShape()) */
1053             return super.impl_computeLayoutBounds();
1054         }
1055         BaseBounds bounds = getLogicalBounds();
1056         double x = bounds.getMinX() + getX();
1057         double y = bounds.getMinY() + getY() + getYAdjustment(bounds);
1058         double width = bounds.getWidth();
1059         double height = bounds.getHeight();
1060         double wrappingWidth = getWrappingWidth();
1061         if (wrappingWidth != 0) width = wrappingWidth;
1062         return new BoundingBox(x, y, width, height);
1063     }
1064 
1065     /**
1066      * @treatAsPrivate implementation detail
1067      * @deprecated This is an internal API that is not intended
1068      * for use and will be removed in the next version
1069      */
1070     @Deprecated
1071     @Override
1072     public final BaseBounds impl_computeGeomBounds(BaseBounds bounds,
1073                                                    BaseTransform tx) {
1074         if (isSpan()) {
1075             if (impl_mode != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) {
1076                 return super.impl_computeGeomBounds(bounds, tx);
1077             }
1078             TextLayout layout = getTextLayout();
1079             bounds = layout.getBounds(getTextSpan(), bounds);
1080             BaseBounds spanBounds = getSpanBounds();
1081             float minX = bounds.getMinX() - spanBounds.getMinX();
1082             float minY = bounds.getMinY() - spanBounds.getMinY();
1083             float maxX = minX + bounds.getWidth();
1084             float maxY = minY + bounds.getHeight();
1085             bounds = bounds.deriveWithNewBounds(minX, minY, 0, maxX, maxY, 0);
1086             return tx.transform(bounds, bounds);
1087         }
1088 
1089         if (getBoundsType() == TextBoundsType.VISUAL) {
1090             if (getTextInternal().length() == 0 || impl_mode == NGShape.Mode.EMPTY) {
1091                 return bounds.makeEmpty();
1092             }
1093             if (impl_mode == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) {
1094                 /* Optimize for FILL and INNER STROKE: save the cost of shaping each glyph */
1095                 BaseBounds visualBounds = getVisualBounds();
1096                 float x = visualBounds.getMinX() + (float) getX();
1097                 float yadj = getYAdjustment(visualBounds);
1098                 float y = visualBounds.getMinY() + yadj + (float) getY();
1099                 bounds.deriveWithNewBounds(x, y, 0, x + visualBounds.getWidth(),
1100                         y + visualBounds.getHeight(), 0);
1101                 return tx.transform(bounds, bounds);
1102             } else {
1103                 /* Let the super class compute the bounds using shape */
1104                 return super.impl_computeGeomBounds(bounds, tx);
1105             }
1106         }
1107 
1108         BaseBounds textBounds = getLogicalBounds();
1109         float x = textBounds.getMinX() + (float)getX();
1110         float yadj = getYAdjustment(textBounds);
1111         float y = textBounds.getMinY() + yadj + (float)getY();
1112         float width = textBounds.getWidth();
1113         float height = textBounds.getHeight();
1114         float wrappingWidth = (float)getWrappingWidth();
1115         if (wrappingWidth > width) {
1116             width = wrappingWidth;
1117         } else {
1118             /* The following adjustment is necessary for the text bounds to be
1119              * relative to the same location as the mirrored bounds returned
1120              * by layout.getBounds().
1121              */
1122             if (wrappingWidth > 0) {
1123                 NodeOrientation orientation = getEffectiveNodeOrientation();
1124                 if (orientation == NodeOrientation.RIGHT_TO_LEFT) {
1125                     x -= width - wrappingWidth;
1126                 }
1127             }
1128         }
1129         textBounds = new RectBounds(x, y, x + width, y + height);
1130 
1131         /* handle stroked text */
1132         if (impl_mode != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) {
1133             bounds =
1134                 super.impl_computeGeomBounds(bounds,
1135                                              BaseTransform.IDENTITY_TRANSFORM);
1136         } else {
1137             TextLayout layout = getTextLayout();
1138             bounds = layout.getBounds(null, bounds);
1139             x = bounds.getMinX() + (float)getX();
1140             width = bounds.getWidth();
1141             bounds = bounds.deriveWithNewBounds(x, y, 0, x + width, y + height, 0);
1142         }
1143 
1144         bounds = bounds.deriveWithUnion(textBounds);
1145         return tx.transform(bounds, bounds);
1146     }
1147 
1148     /**
1149      * @treatAsPrivate implementation detail
1150      * @deprecated This is an internal API that is not intended
1151      * for use and will be removed in the next version
1152      */
1153     @Deprecated
1154     @Override
1155     protected final boolean impl_computeContains(double localX, double localY) {
1156         /* Used for spans, regular text uses bounds based picking */
1157         double x = localX + getSpanBounds().getMinX();
1158         double y = localY + getSpanBounds().getMinY();
1159         GlyphList[] runs = getRuns();
1160         if (runs.length != 0) {
1161             for (int i = 0; i < runs.length; i++) {
1162                 GlyphList run = runs[i];
1163                 com.sun.javafx.geom.Point2D location = run.getLocation();
1164                 float width = run.getWidth();
1165                 RectBounds lineBounds = run.getLineBounds();
1166                 float height = lineBounds.getHeight();
1167                 if (location.x <= x && x < location.x + width &&
1168                     location.y <= y && y < location.y + height) {
1169                         return true;
1170                 }
1171             }
1172         }
1173         return false;
1174     }
1175 
1176     /**
1177      * @treatAsPrivate implementation detail
1178      * @deprecated This is an internal API that is not intended
1179      * for use and will be removed in the next version
1180      */
1181     @Deprecated
1182     @Override
1183     public final com.sun.javafx.geom.Shape impl_configShape() {
1184         if (impl_mode == NGShape.Mode.EMPTY || getTextInternal().length() == 0) {
1185             return new Path2D();
1186         }
1187         com.sun.javafx.geom.Shape shape = getShape();
1188         float x, y;
1189         if (isSpan()) {
1190             BaseBounds bounds = getSpanBounds();
1191             x = -bounds.getMinX();
1192             y = -bounds.getMinY();
1193         } else {
1194             x = (float)getX();
1195             y = getYAdjustment(getVisualBounds()) + (float)getY();
1196         }
1197         return TransformedShape.translatedShape(shape, x, y);
1198     }
1199 
1200    /***************************************************************************
1201     *                                                                         *
1202     *                            Stylesheet Handling                          *
1203     *                                                                         *
1204     **************************************************************************/
1205 
1206      /**
1207       * Super-lazy instantiation pattern from Bill Pugh.
1208       * @treatAsPrivate implementation detail
1209       */
1210      private static class StyleableProperties {
1211 
1212          private static final CssMetaData<Text,Font> FONT =
1213             new FontCssMetaData<Text>("-fx-font", Font.getDefault()) {
1214 
1215             @Override
1216             public boolean isSettable(Text node) {
1217                 return node.font == null || !node.font.isBound();
1218             }
1219 
1220             @Override
1221             public StyleableProperty<Font> getStyleableProperty(Text node) {
1222                 return (StyleableProperty<Font>)node.fontProperty();
1223             }
1224          };
1225 
1226          private static final CssMetaData<Text,Boolean> UNDERLINE =
1227             new CssMetaData<Text,Boolean>("-fx-underline",
1228                  BooleanConverter.getInstance(), Boolean.FALSE) {
1229 
1230             @Override
1231             public boolean isSettable(Text node) {
1232                 return node.attributes == null ||
1233                        node.attributes.underline == null ||
1234                       !node.attributes.underline.isBound();
1235             }
1236 
1237             @Override
1238             public StyleableProperty<Boolean> getStyleableProperty(Text node) {
1239                 return (StyleableProperty<Boolean>)node.underlineProperty();
1240             }
1241          };
1242 
1243          private static final CssMetaData<Text,Boolean> STRIKETHROUGH =
1244             new CssMetaData<Text,Boolean>("-fx-strikethrough",
1245                  BooleanConverter.getInstance(), Boolean.FALSE) {
1246 
1247             @Override
1248             public boolean isSettable(Text node) {
1249                 return node.attributes == null ||
1250                        node.attributes.strikethrough == null ||
1251                       !node.attributes.strikethrough.isBound();
1252             }
1253 
1254             @Override
1255             public StyleableProperty<Boolean> getStyleableProperty(Text node) {
1256                 return (StyleableProperty<Boolean>)node.strikethroughProperty();
1257             }
1258          };
1259 
1260          private static final
1261              CssMetaData<Text,TextAlignment> TEXT_ALIGNMENT =
1262                  new CssMetaData<Text,TextAlignment>("-fx-text-alignment",
1263                  new EnumConverter<TextAlignment>(TextAlignment.class),
1264                  TextAlignment.LEFT) {
1265 
1266             @Override
1267             public boolean isSettable(Text node) {
1268                 return node.attributes == null ||
1269                        node.attributes.textAlignment == null ||
1270                       !node.attributes.textAlignment.isBound();
1271             }
1272 
1273             @Override
1274             public StyleableProperty<TextAlignment> getStyleableProperty(Text node) {
1275                 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty();
1276             }
1277          };
1278 
1279          private static final CssMetaData<Text,VPos> TEXT_ORIGIN =
1280                  new CssMetaData<Text,VPos>("-fx-text-origin",
1281                  new EnumConverter<VPos>(VPos.class),
1282                  VPos.BASELINE) {
1283 
1284             @Override
1285             public boolean isSettable(Text node) {
1286                 return node.attributes == null ||
1287                        node.attributes.textOrigin == null ||
1288                       !node.attributes.textOrigin.isBound();
1289             }
1290 
1291             @Override
1292             public StyleableProperty<VPos> getStyleableProperty(Text node) {
1293                 return (StyleableProperty<VPos>)node.textOriginProperty();
1294             }
1295          };
1296 
1297          private static final CssMetaData<Text,FontSmoothingType>
1298              FONT_SMOOTHING_TYPE =
1299              new CssMetaData<Text,FontSmoothingType>(
1300                  "-fx-font-smoothing-type",
1301                  new EnumConverter<FontSmoothingType>(FontSmoothingType.class),
1302                  FontSmoothingType.GRAY) {
1303 
1304             @Override
1305             public boolean isSettable(Text node) {
1306                 return node.fontSmoothingType == null ||
1307                        !node.fontSmoothingType.isBound();
1308             }
1309 
1310             @Override
1311             public StyleableProperty<FontSmoothingType>
1312                                  getStyleableProperty(Text node) {
1313 
1314                 return (StyleableProperty<FontSmoothingType>)node.fontSmoothingTypeProperty();
1315             }
1316          };
1317 
1318          private static final
1319              CssMetaData<Text,Number> LINE_SPACING =
1320                  new CssMetaData<Text,Number>("-fx-line-spacing",
1321                  SizeConverter.getInstance(), 0) {
1322 
1323             @Override
1324             public boolean isSettable(Text node) {
1325                 return node.attributes == null ||
1326                        node.attributes.lineSpacing == null ||
1327                       !node.attributes.lineSpacing.isBound();
1328             }
1329 
1330             @Override
1331             public StyleableProperty<Number> getStyleableProperty(Text node) {
1332                 return (StyleableProperty<Number>)node.lineSpacingProperty();
1333             }
1334          };
1335 
1336          private static final CssMetaData<Text, TextBoundsType>
1337              BOUNDS_TYPE =
1338              new CssMetaData<Text,TextBoundsType>(
1339                  "-fx-bounds-type",
1340                  new EnumConverter<TextBoundsType>(TextBoundsType.class),
1341                  DEFAULT_BOUNDS_TYPE) {
1342 
1343             @Override
1344             public boolean isSettable(Text node) {
1345                 return node.boundsType == null || !node.boundsType.isBound();
1346             }
1347 
1348             @Override
1349             public StyleableProperty<TextBoundsType> getStyleableProperty(Text node) {
1350                 return (StyleableProperty<TextBoundsType>)node.boundsTypeProperty();
1351             }
1352          };
1353 
1354      private final static List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1355          static {
1356             final List<CssMetaData<? extends Styleable, ?>> styleables =
1357                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Shape.getClassCssMetaData());
1358             styleables.add(FONT);
1359             styleables.add(UNDERLINE);
1360             styleables.add(STRIKETHROUGH);
1361             styleables.add(TEXT_ALIGNMENT);
1362             styleables.add(TEXT_ORIGIN);
1363             styleables.add(FONT_SMOOTHING_TYPE);
1364             styleables.add(LINE_SPACING);
1365             styleables.add(BOUNDS_TYPE);
1366             STYLEABLES = Collections.unmodifiableList(styleables);
1367          }
1368     }
1369 
1370     /**
1371      * @return The CssMetaData associated with this class, which may include the
1372      * CssMetaData of its super classes.
1373      * @since JavaFX 8.0
1374      */
1375     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1376         return StyleableProperties.STYLEABLES;
1377     }
1378 
1379     /**
1380      * {@inheritDoc}
1381      *
1382      * @since JavaFX 8.0
1383      */
1384 
1385 
1386     @Override
1387     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1388         return getClassCssMetaData();
1389     }
1390 
1391     @SuppressWarnings("deprecation")
1392     private void updatePGText() {
1393         final NGText peer = impl_getPeer();
1394         if (impl_isDirty(DirtyBits.TEXT_ATTRS)) {
1395             peer.setUnderline(isUnderline());
1396             peer.setStrikethrough(isStrikethrough());
1397             FontSmoothingType smoothing = getFontSmoothingType();
1398             if (smoothing == null) smoothing = FontSmoothingType.GRAY;
1399             peer.setFontSmoothingType(smoothing.ordinal());
1400         }
1401         if (impl_isDirty(DirtyBits.TEXT_FONT)) {
1402             peer.setFont(getFontInternal());
1403         }
1404         if (impl_isDirty(DirtyBits.NODE_CONTENTS)) {
1405             peer.setGlyphs(getRuns());
1406         }
1407         if (impl_isDirty(DirtyBits.NODE_GEOMETRY)) {
1408             if (isSpan()) {
1409                 BaseBounds spanBounds = getSpanBounds();
1410                 peer.setLayoutLocation(spanBounds.getMinX(), spanBounds.getMinY());
1411             } else {
1412                 float x = (float)getX();
1413                 float y = (float)getY();
1414                 float yadj = getYRendering();
1415                 peer.setLayoutLocation(-x, yadj - y);
1416             }
1417         }
1418         if (impl_isDirty(DirtyBits.TEXT_SELECTION)) {
1419             Object fillObj = null;
1420             int start = getSelectionStart();
1421             int end = getSelectionEnd();
1422             int length = getTextInternal().length();
1423             if (0 <= start && start < end  && end <= length) {
1424                 Paint fill = selectionFillProperty().get();
1425                 fillObj = fill != null ? Toolkit.getPaintAccessor().getPlatformPaint(fill) : null;
1426             }
1427             peer.setSelection(start, end, fillObj);
1428         }
1429     }
1430 
1431     /**
1432      * @treatAsPrivate implementation detail
1433      * @deprecated This is an internal API that is not intended
1434      * for use and will be removed in the next version
1435      */
1436     @Deprecated
1437     @Override
1438     public final void impl_updatePeer() {
1439         super.impl_updatePeer();
1440         updatePGText();
1441     }
1442 
1443     /***************************************************************************
1444      *                                                                         *
1445      *                       Seldom Used Properties                            *
1446      *                                                                         *
1447      **************************************************************************/
1448 
1449     private TextAttribute attributes;
1450 
1451     private TextAttribute getTextAttribute() {
1452         if (attributes == null) {
1453             attributes = new TextAttribute();
1454         }
1455         return attributes;
1456     }
1457 
1458     private static final VPos DEFAULT_TEXT_ORIGIN = VPos.BASELINE;
1459     private static final TextBoundsType DEFAULT_BOUNDS_TYPE = TextBoundsType.LOGICAL;
1460     private static final boolean DEFAULT_UNDERLINE = false;
1461     private static final boolean DEFAULT_STRIKETHROUGH = false;
1462     private static final TextAlignment DEFAULT_TEXT_ALIGNMENT = TextAlignment.LEFT;
1463     private static final double DEFAULT_LINE_SPACING = 0;
1464     private static final int DEFAULT_CARET_POSITION = -1;
1465     private static final int DEFAULT_SELECTION_START = -1;
1466     private static final int DEFAULT_SELECTION_END = -1;
1467     private static final Color DEFAULT_SELECTION_FILL= Color.WHITE;
1468     private static final boolean DEFAULT_CARET_BIAS = true;
1469 
1470     private final class TextAttribute {
1471 
1472         private ObjectProperty<VPos> textOrigin;
1473 
1474         public final VPos getTextOrigin() {
1475             return textOrigin == null ? DEFAULT_TEXT_ORIGIN : textOrigin.get();
1476         }
1477 
1478         public final ObjectProperty<VPos> textOriginProperty() {
1479             if (textOrigin == null) {
1480                 textOrigin = new StyleableObjectProperty<VPos>(DEFAULT_TEXT_ORIGIN) {
1481                     @Override public Object getBean() { return Text.this; }
1482                     @Override public String getName() { return "textOrigin"; }
1483                     @Override public CssMetaData getCssMetaData() {
1484                         return StyleableProperties.TEXT_ORIGIN;
1485                     }
1486                     @Override public void invalidated() {
1487                         impl_geomChanged();
1488                     }
1489                 };
1490             }
1491             return textOrigin;
1492         }
1493 
1494         private BooleanProperty underline;
1495 
1496         public final boolean isUnderline() {
1497             return underline == null ? DEFAULT_UNDERLINE : underline.get();
1498         }
1499 
1500         public final BooleanProperty underlineProperty() {
1501             if (underline == null) {
1502                 underline = new StyleableBooleanProperty() {
1503                     @Override public Object getBean() { return Text.this; }
1504                     @Override public String getName() { return "underline"; }
1505                     @Override public CssMetaData getCssMetaData() {
1506                         return StyleableProperties.UNDERLINE;
1507                     }
1508                     @Override public void invalidated() {
1509                         impl_markDirty(DirtyBits.TEXT_ATTRS);
1510                         if (getBoundsType() == TextBoundsType.VISUAL) {
1511                             impl_geomChanged();
1512                         }
1513                     }
1514                 };
1515             }
1516             return underline;
1517         }
1518 
1519         private BooleanProperty strikethrough;
1520 
1521         public final boolean isStrikethrough() {
1522             return strikethrough == null ? DEFAULT_STRIKETHROUGH : strikethrough.get();
1523         }
1524 
1525         public final BooleanProperty strikethroughProperty() {
1526             if (strikethrough == null) {
1527                 strikethrough = new StyleableBooleanProperty() {
1528                     @Override public Object getBean() { return Text.this; }
1529                     @Override public String getName() { return "strikethrough"; }
1530                     @Override public CssMetaData getCssMetaData() {
1531                         return StyleableProperties.STRIKETHROUGH;
1532                     }
1533                     @Override public void invalidated() {
1534                         impl_markDirty(DirtyBits.TEXT_ATTRS);
1535                         if (getBoundsType() == TextBoundsType.VISUAL) {
1536                             impl_geomChanged();
1537                         }
1538                     }
1539                 };
1540             }
1541             return strikethrough;
1542         }
1543 
1544         private ObjectProperty<TextAlignment> textAlignment;
1545 
1546         public final TextAlignment getTextAlignment() {
1547             return textAlignment == null ? DEFAULT_TEXT_ALIGNMENT : textAlignment.get();
1548         }
1549 
1550         public final ObjectProperty<TextAlignment> textAlignmentProperty() {
1551             if (textAlignment == null) {
1552                 textAlignment =
1553                     new StyleableObjectProperty<TextAlignment>(DEFAULT_TEXT_ALIGNMENT) {
1554                     @Override public Object getBean() { return Text.this; }
1555                     @Override public String getName() { return "textAlignment"; }
1556                     @Override public CssMetaData getCssMetaData() {
1557                         return StyleableProperties.TEXT_ALIGNMENT;
1558                     }
1559                     @Override public void invalidated() {
1560                         if (!isSpan()) {
1561                             TextAlignment alignment = get();
1562                             if (alignment == null) {
1563                                 alignment = DEFAULT_TEXT_ALIGNMENT;
1564                             }
1565                             TextLayout layout = getTextLayout();
1566                             if (layout.setAlignment(alignment.ordinal())) {
1567                                 needsTextLayout();
1568                             }
1569                         }
1570                     }
1571                 };
1572             }
1573             return textAlignment;
1574         }
1575 
1576         private DoubleProperty lineSpacing;
1577 
1578         public final double getLineSpacing() {
1579             return lineSpacing == null ? DEFAULT_LINE_SPACING : lineSpacing.get();
1580         }
1581 
1582         public final DoubleProperty lineSpacingProperty() {
1583             if (lineSpacing == null) {
1584                 lineSpacing =
1585                     new StyleableDoubleProperty(DEFAULT_LINE_SPACING) {
1586                     @Override public Object getBean() { return Text.this; }
1587                     @Override public String getName() { return "lineSpacing"; }
1588                     @Override public CssMetaData getCssMetaData() {
1589                         return StyleableProperties.LINE_SPACING;
1590                     }
1591                     @Override public void invalidated() {
1592                         if (!isSpan()) {
1593                             TextLayout layout = getTextLayout();
1594                             if (layout.setLineSpacing((float)get())) {
1595                                 needsTextLayout();
1596                             }
1597                         }
1598                     }
1599                 };
1600             }
1601             return lineSpacing;
1602         }
1603 
1604         private ReadOnlyDoubleWrapper baselineOffset;
1605 
1606         public final ReadOnlyDoubleProperty baselineOffsetProperty() {
1607             if (baselineOffset == null) {
1608                 baselineOffset = new ReadOnlyDoubleWrapper(Text.this, "baselineOffset") {
1609                     {bind(new DoubleBinding() {
1610                         {bind(fontProperty());}
1611                         @Override protected double computeValue() {
1612                             /* This method should never be used for spans.
1613                              * If it is, it will still returns the ascent
1614                              * for the first line in the layout */
1615                             BaseBounds bounds = getLogicalBounds();
1616                             return -bounds.getMinY();
1617                         }
1618                     });}
1619                 };
1620             }
1621             return baselineOffset.getReadOnlyProperty();
1622         }
1623 
1624         @Deprecated
1625         private ObjectProperty<PathElement[]> impl_selectionShape;
1626         private ObjectBinding<PathElement[]> impl_selectionBinding;
1627 
1628         @Deprecated
1629         public final ReadOnlyObjectProperty<PathElement[]> impl_selectionShapeProperty() {
1630             if (impl_selectionShape == null) {
1631                 impl_selectionBinding = new ObjectBinding<PathElement[]>() {
1632                     {bind(impl_selectionStartProperty(), impl_selectionEndProperty());}
1633                     @Override protected PathElement[] computeValue() {
1634                         int start = getSelectionStart();
1635                         int end = getSelectionEnd();
1636                         return getRange(start, end, TextLayout.TYPE_TEXT);
1637                     }
1638               };
1639               impl_selectionShape = new SimpleObjectProperty<PathElement[]>(Text.this, "impl_selectionShape");
1640               impl_selectionShape.bind(impl_selectionBinding);
1641             }
1642             return impl_selectionShape;
1643         }
1644 
1645         private ObjectProperty<Paint> selectionFill;
1646 
1647         @Deprecated
1648         public final ObjectProperty<Paint> impl_selectionFillProperty() {
1649             if (selectionFill == null) {
1650                 selectionFill =
1651                     new ObjectPropertyBase<Paint>(DEFAULT_SELECTION_FILL) {
1652                         @Override public Object getBean() { return Text.this; }
1653                         @Override public String getName() { return "impl_selectionFill"; }
1654                         @Override protected void invalidated() {
1655                             impl_markDirty(DirtyBits.TEXT_SELECTION);
1656                         }
1657                     };
1658             }
1659             return selectionFill;
1660         }
1661 
1662         @Deprecated
1663         private IntegerProperty impl_selectionStart;
1664 
1665         @Deprecated
1666         public final int getImpl_selectionStart() {
1667             return impl_selectionStart == null ? DEFAULT_SELECTION_START : impl_selectionStart.get();
1668         }
1669 
1670         @Deprecated
1671         public final IntegerProperty impl_selectionStartProperty() {
1672             if (impl_selectionStart == null) {
1673                 impl_selectionStart =
1674                     new IntegerPropertyBase(DEFAULT_SELECTION_START) {
1675                         @Override public Object getBean() { return Text.this; }
1676                         @Override public String getName() { return "impl_selectionStart"; }
1677                         @Override protected void invalidated() {
1678                             impl_markDirty(DirtyBits.TEXT_SELECTION);
1679                             notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_START);
1680                         }
1681                 };
1682             }
1683             return impl_selectionStart;
1684         }
1685 
1686         @Deprecated
1687         private IntegerProperty impl_selectionEnd;
1688 
1689         @Deprecated
1690         public final int getImpl_selectionEnd() {
1691             return impl_selectionEnd == null ? DEFAULT_SELECTION_END : impl_selectionEnd.get();
1692         }
1693 
1694         @Deprecated
1695         public final IntegerProperty impl_selectionEndProperty() {
1696             if (impl_selectionEnd == null) {
1697                 impl_selectionEnd =
1698                     new IntegerPropertyBase(DEFAULT_SELECTION_END) {
1699                         @Override public Object getBean() { return Text.this; }
1700                         @Override public String getName() { return "impl_selectionEnd"; }
1701                         @Override protected void invalidated() {
1702                             impl_markDirty(DirtyBits.TEXT_SELECTION);
1703                             notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END);
1704                         }
1705                     };
1706             }
1707             return impl_selectionEnd;
1708         }
1709 
1710         private ObjectProperty<PathElement[]> impl_caretShape;
1711         private ObjectBinding<PathElement[]> impl_caretBinding;
1712 
1713         @Deprecated
1714         public final ReadOnlyObjectProperty<PathElement[]> impl_caretShapeProperty() {
1715             if (impl_caretShape == null) {
1716                 impl_caretBinding = new ObjectBinding<PathElement[]>() {
1717                     {bind(impl_caretPositionProperty(), impl_caretBiasProperty());}
1718                     @Override protected PathElement[] computeValue() {
1719                         int pos = getImpl_caretPosition();
1720                         int length = getTextInternal().length();
1721                         if (0 <= pos && pos <= length) {
1722                             boolean bias = isImpl_caretBias();
1723                             float x = (float)getX();
1724                             float y = (float)getY() - getYRendering();
1725                             TextLayout layout = getTextLayout();
1726                             return layout.getCaretShape(pos, bias, x, y);
1727                         }
1728                         return EMPTY_PATH_ELEMENT_ARRAY;
1729                     }
1730                 };
1731                 impl_caretShape = new SimpleObjectProperty<PathElement[]>(Text.this, "impl_caretShape");
1732                 impl_caretShape.bind(impl_caretBinding);
1733             }
1734             return impl_caretShape;
1735         }
1736 
1737         @Deprecated
1738         private IntegerProperty impl_caretPosition;
1739 
1740         @Deprecated
1741         public final int getImpl_caretPosition() {
1742             return impl_caretPosition == null ? DEFAULT_CARET_POSITION : impl_caretPosition.get();
1743         }
1744 
1745         @Deprecated
1746         public final IntegerProperty impl_caretPositionProperty() {
1747             if (impl_caretPosition == null) {
1748                 impl_caretPosition =
1749                     new IntegerPropertyBase(DEFAULT_CARET_POSITION) {
1750                         @Override public Object getBean() { return Text.this; }
1751                         @Override public String getName() { return "impl_caretPosition"; }
1752                         @Override protected void invalidated() {
1753                             notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END);
1754                         }
1755                     };
1756             }
1757             return impl_caretPosition;
1758         }
1759 
1760         @Deprecated
1761         private BooleanProperty impl_caretBias;
1762 
1763         @Deprecated
1764         public final boolean isImpl_caretBias() {
1765             return impl_caretBias == null ? DEFAULT_CARET_BIAS : impl_caretBias.get();
1766         }
1767 
1768         @Deprecated
1769         public final BooleanProperty impl_caretBiasProperty() {
1770             if (impl_caretBias == null) {
1771                 impl_caretBias =
1772                         new SimpleBooleanProperty(Text.this, "impl_caretBias", DEFAULT_CARET_BIAS);
1773             }
1774             return impl_caretBias;
1775         }
1776     }
1777 
1778     /**
1779      * Returns a string representation of this {@code Text} object.
1780      * @return a string representation of this {@code Text} object.
1781      */
1782     @Override
1783     public String toString() {
1784         final StringBuilder sb = new StringBuilder("Text[");
1785 
1786         String id = getId();
1787         if (id != null) {
1788             sb.append("id=").append(id).append(", ");
1789         }
1790 
1791         sb.append("text=\"").append(getText()).append("\"");
1792         sb.append(", x=").append(getX());
1793         sb.append(", y=").append(getY());
1794         sb.append(", alignment=").append(getTextAlignment());
1795         sb.append(", origin=").append(getTextOrigin());
1796         sb.append(", boundsType=").append(getBoundsType());
1797 
1798         double spacing = getLineSpacing();
1799         if (spacing != DEFAULT_LINE_SPACING) {
1800             sb.append(", lineSpacing=").append(spacing);
1801         }
1802 
1803         double wrap = getWrappingWidth();
1804         if (wrap != 0) {
1805             sb.append(", wrappingWidth=").append(wrap);
1806         }
1807 
1808         sb.append(", font=").append(getFont());
1809         sb.append(", fontSmoothingType=").append(getFontSmoothingType());
1810 
1811         if (isStrikethrough()) {
1812             sb.append(", strikethrough");
1813         }
1814         if (isUnderline()) {
1815             sb.append(", underline");
1816         }
1817 
1818         sb.append(", fill=").append(getFill());
1819 
1820         Paint stroke = getStroke();
1821         if (stroke != null) {
1822             sb.append(", stroke=").append(stroke);
1823             sb.append(", strokeWidth=").append(getStrokeWidth());
1824         }
1825 
1826         return sb.append("]").toString();
1827     }
1828 
1829     @Override
1830     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1831         switch (attribute) {
1832             case TEXT: {
1833                 String accText = getAccessibleText();
1834                 if (accText != null && !accText.isEmpty()) return accText;
1835                 return getText();
1836             }
1837             case FONT: return getFont();
1838             case CARET_OFFSET: {
1839                 int sel = getCaretPosition();
1840                 if (sel >=  0) return sel;
1841                 return getText().length();
1842             }
1843             case SELECTION_START: {
1844                 int sel = getSelectionStart();
1845                 if (sel >=  0) return sel;
1846                 sel = getCaretPosition();
1847                 if (sel >=  0) return sel;
1848                 return getText().length();
1849             }
1850             case SELECTION_END:  {
1851                 int sel = getSelectionEnd();
1852                 if (sel >=  0) return sel;
1853                 sel = getCaretPosition();
1854                 if (sel >=  0) return sel;
1855                 return getText().length();
1856             }
1857             case LINE_FOR_OFFSET: {
1858                 int offset = (Integer)parameters[0];
1859                 if (offset > getTextInternal().length()) return null;
1860                 TextLine[] lines = getTextLayout().getLines();
1861                 int lineIndex = 0;
1862                 for (int i = 1; i < lines.length; i++) {
1863                     TextLine line = lines[i];
1864                     if (line.getStart() > offset) break;
1865                     lineIndex++;
1866                 }
1867                 return lineIndex;
1868             }
1869             case LINE_START: {
1870                 int lineIndex = (Integer)parameters[0];
1871                 TextLine[] lines = getTextLayout().getLines();
1872                 if (0 <= lineIndex && lineIndex < lines.length) {
1873                     TextLine line = lines[lineIndex];
1874                     return line.getStart();
1875                 }
1876                 return null;
1877             }
1878             case LINE_END: {
1879                 int lineIndex = (Integer)parameters[0];
1880                 TextLine[] lines = getTextLayout().getLines();
1881                 if (0 <= lineIndex && lineIndex < lines.length) {
1882                     TextLine line = lines[lineIndex];
1883                     return line.getStart() + line.getLength();
1884                 }
1885                 return null;
1886             }
1887             case OFFSET_AT_POINT: {
1888                 Point2D point = (Point2D)parameters[0];
1889                 point = screenToLocal(point);
1890                 return hitTest(point).getCharIndex();
1891             }
1892             case BOUNDS_FOR_RANGE: {
1893                 int start = (Integer)parameters[0];
1894                 int end = (Integer)parameters[1];
1895                 PathElement[] elements = rangeShape(start, end + 1);
1896                 /* Each bounds is defined by a MoveTo (top-left) followed by
1897                  * 4 LineTo (to top-right, bottom-right, bottom-left, back to top-left).
1898                  */
1899                 Bounds[] bounds = new Bounds[elements.length / 5];
1900                 int index = 0;
1901                 for (int i = 0; i < bounds.length; i++) {
1902                     MoveTo topLeft = (MoveTo)elements[index];
1903                     LineTo topRight = (LineTo)elements[index+1];
1904                     LineTo bottomRight = (LineTo)elements[index+2];
1905                     BoundingBox b = new BoundingBox(topLeft.getX(), topLeft.getY(),
1906                                                     topRight.getX() - topLeft.getX(),
1907                                                     bottomRight.getY() - topRight.getY());
1908                     bounds[i] = localToScreen(b);
1909                     index += 5;
1910                 }
1911                 return bounds;
1912             }
1913             default: return super.queryAccessibleAttribute(attribute, parameters);
1914         }
1915     }
1916 }