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