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