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