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