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