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