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