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