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