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 com.sun.javafx.geom.Shape doConfigShape(Shape shape) { 119 return ((Text) shape).doConfigShape(); 120 } 121 }); 122 } 123 124 private TextLayout layout; 125 private static final PathElement[] EMPTY_PATH_ELEMENT_ARRAY = new PathElement[0]; 126 127 { 128 // To initialize the class helper at the begining each constructor of this class 129 TextHelper.initHelper(this); 130 } 131 132 /** 133 * Creates an empty instance of Text. 134 */ 135 public Text() { 136 setAccessibleRole(AccessibleRole.TEXT); 137 InvalidationListener listener = observable -> checkSpan(); 190 int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR; 191 TextLayout layout = getTextLayout(); 192 if (layout.setDirection(dir)) { 193 needsTextLayout(); 194 } 195 } 196 } 197 198 @Override 199 public boolean usesMirroring() { 200 return false; 201 } 202 203 private void needsFullTextLayout() { 204 if (isSpan()) { 205 /* Create new text span every time the font or text changes 206 * so the text layout can see that the content has changed. 207 */ 208 textSpan = null; 209 210 /* Relies on impl_geomChanged() to request text flow to relayout */ 211 } else { 212 TextLayout layout = getTextLayout(); 213 String string = getTextInternal(); 214 Object font = getFontInternal(); 215 layout.setContent(string, font); 216 } 217 needsTextLayout(); 218 } 219 220 private void needsTextLayout() { 221 textRuns = null; 222 impl_geomChanged(); 223 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); 224 } 225 226 private TextSpan textSpan; 227 TextSpan getTextSpan() { 228 if (textSpan == null) { 229 textSpan = new TextSpan() { 230 @Override public String getText() { 231 return getTextInternal(); 232 } 233 @Override public Object getFont() { 234 return getFontInternal(); 235 } 236 @Override public RectBounds getBounds() { 237 return null; 238 } 239 }; 240 } 241 return textSpan; 242 } 278 GlyphList run = runs[i]; 279 if (run.getTextSpan() == span) { 280 count++; 281 } 282 } 283 textRuns = new GlyphList[count]; 284 count = 0; 285 for (int i = 0; i < runs.length; i++) { 286 GlyphList run = runs[i]; 287 if (run.getTextSpan() == span) { 288 textRuns[count++] = run; 289 } 290 } 291 spanBoundsInvalid = true; 292 293 /* Sometimes a property change in the text node will causes layout in 294 * text flow. In this case all the dirty bits are already clear and no 295 * extra work is necessary. Other times the layout is caused by changes 296 * in the text flow object (wrapping width and text alignment for example). 297 * In the second case the dirty bits must be set here using 298 * impl_geomChanged() and NodeHelper.markDirty(). Note that impl_geomChanged() 299 * causes another (undesired) layout request in the parent. 300 * In general this is not a problem because shapes are not resizable and 301 * region objects do not propagate layout changes to the parent. 302 * This is a special case where a shape is resized by the parent during 303 * layoutChildren(). See TextFlow#requestLayout() for information how 304 * text flow deals with this situation. 305 */ 306 impl_geomChanged(); 307 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); 308 } 309 310 BaseBounds getSpanBounds() { 311 if (spanBoundsInvalid) { 312 GlyphList[] runs = getRuns(); 313 if (runs.length != 0) { 314 float left = Float.POSITIVE_INFINITY; 315 float top = Float.POSITIVE_INFINITY; 316 float right = 0; 317 float bottom = 0; 318 for (int i = 0; i < runs.length; i++) { 319 GlyphList run = runs[i]; 320 com.sun.javafx.geom.Point2D location = run.getLocation(); 321 float width = run.getWidth(); 322 float height = run.getLineBounds().getHeight(); 323 left = Math.min(location.x, left); 324 top = Math.min(location.y, top); 325 right = Math.max(location.x + width, right); 326 bottom = Math.max(location.y + height, bottom); 439 * Defines the X coordinate of text origin. 440 * 441 * @defaultValue 0 442 */ 443 private DoubleProperty x; 444 445 public final void setX(double value) { 446 xProperty().set(value); 447 } 448 449 public final double getX() { 450 return x == null ? 0.0 : x.get(); 451 } 452 453 public final DoubleProperty xProperty() { 454 if (x == null) { 455 x = new DoublePropertyBase() { 456 @Override public Object getBean() { return Text.this; } 457 @Override public String getName() { return "x"; } 458 @Override public void invalidated() { 459 impl_geomChanged(); 460 } 461 }; 462 } 463 return x; 464 } 465 466 /** 467 * Defines the Y coordinate of text origin. 468 * 469 * @defaultValue 0 470 */ 471 private DoubleProperty y; 472 473 public final void setY(double value) { 474 yProperty().set(value); 475 } 476 477 public final double getY() { 478 return y == null ? 0.0 : y.get(); 479 } 480 481 public final DoubleProperty yProperty() { 482 if (y == null) { 483 y = new DoublePropertyBase() { 484 @Override public Object getBean() { return Text.this; } 485 @Override public String getName() { return "y"; } 486 @Override public void invalidated() { 487 impl_geomChanged(); 488 } 489 }; 490 } 491 return y; 492 } 493 494 /** 495 * Defines the font of text. 496 * 497 * @defaultValue Font{} 498 */ 499 private ObjectProperty<Font> font; 500 501 public final void setFont(Font value) { 502 fontProperty().set(value); 503 } 504 505 public final Font getFont() { 506 return font == null ? Font.getDefault() : font.get(); 507 } 576 } 577 578 public final ObjectProperty<TextBoundsType> boundsTypeProperty() { 579 if (boundsType == null) { 580 boundsType = 581 new StyleableObjectProperty<TextBoundsType>(DEFAULT_BOUNDS_TYPE) { 582 @Override public Object getBean() { return Text.this; } 583 @Override public String getName() { return "boundsType"; } 584 @Override public CssMetaData<Text,TextBoundsType> getCssMetaData() { 585 return StyleableProperties.BOUNDS_TYPE; 586 } 587 @Override public void invalidated() { 588 TextLayout layout = getTextLayout(); 589 int type = 0; 590 if (boundsType.get() == TextBoundsType.LOGICAL_VERTICAL_CENTER) { 591 type |= TextLayout.BOUNDS_CENTER; 592 } 593 if (layout.setBoundsType(type)) { 594 needsTextLayout(); 595 } else { 596 impl_geomChanged(); 597 } 598 } 599 }; 600 } 601 return boundsType; 602 } 603 604 /** 605 * Defines a width constraint for the text in user space coordinates, 606 * e.g. pixels, not glyph or character count. 607 * If the value is {@code > 0} text will be line wrapped as needed 608 * to satisfy this constraint. 609 * 610 * @defaultValue 0 611 */ 612 private DoubleProperty wrappingWidth; 613 614 public final void setWrappingWidth(double value) { 615 wrappingWidthProperty().set(value); 616 } 617 618 public final double getWrappingWidth() { 619 return wrappingWidth == null ? 0 : wrappingWidth.get(); 620 } 621 622 public final DoubleProperty wrappingWidthProperty() { 623 if (wrappingWidth == null) { 624 wrappingWidth = new DoublePropertyBase() { 625 @Override public Object getBean() { return Text.this; } 626 @Override public String getName() { return "wrappingWidth"; } 627 @Override public void invalidated() { 628 if (!isSpan()) { 629 TextLayout layout = getTextLayout(); 630 if (layout.setWrapWidth((float)get())) { 631 needsTextLayout(); 632 } else { 633 impl_geomChanged(); 634 } 635 } 636 } 637 }; 638 } 639 return wrappingWidth; 640 } 641 642 public final void setUnderline(boolean value) { 643 underlineProperty().set(value); 644 } 645 646 public final boolean isUnderline() { 647 if (attributes == null || attributes.underline == null) { 648 return DEFAULT_UNDERLINE; 649 } 650 return attributes.isUnderline(); 651 } 652 653 /** 759 } 760 761 public final FontSmoothingType getFontSmoothingType() { 762 return fontSmoothingType == null ? 763 FontSmoothingType.GRAY : fontSmoothingType.get(); 764 } 765 766 public final ObjectProperty<FontSmoothingType> 767 fontSmoothingTypeProperty() { 768 if (fontSmoothingType == null) { 769 fontSmoothingType = 770 new StyleableObjectProperty<FontSmoothingType> 771 (FontSmoothingType.GRAY) { 772 @Override public Object getBean() { return Text.this; } 773 @Override public String getName() { return "fontSmoothingType"; } 774 @Override public CssMetaData<Text,FontSmoothingType> getCssMetaData() { 775 return StyleableProperties.FONT_SMOOTHING_TYPE; 776 } 777 @Override public void invalidated() { 778 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS); 779 impl_geomChanged(); 780 } 781 }; 782 } 783 return fontSmoothingType; 784 } 785 786 /** 787 * @treatAsPrivate implementation detail 788 * @deprecated This is an internal API that is not intended 789 * for use and will be removed in the next version 790 */ 791 @Deprecated 792 @Override 793 protected final void impl_geomChanged() { 794 super.impl_geomChanged(); 795 if (attributes != null) { 796 if (attributes.caretBinding != null) { 797 attributes.caretBinding.invalidate(); 798 } 799 if (attributes.selectionBinding != null) { 800 attributes.selectionBinding.invalidate(); 801 } 802 } 803 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY); 804 } 805 806 /** 807 * Shape of selection in local coordinates. 808 * 809 * @since 9 810 */ 811 public final PathElement[] getSelectionShape() { 812 return selectionShapeProperty().get(); 813 } 814 1038 BaseBounds vBounds = getVisualBounds(); 1039 float delta = vBounds.getMinY() - bounds.getMinY(); 1040 switch (origin) { 1041 case TOP: return delta; 1042 case BASELINE: return -vBounds.getMinY() + delta; 1043 case CENTER: return vBounds.getHeight() / 2 + delta; 1044 case BOTTOM: return vBounds.getHeight() + delta; 1045 default: return 0; 1046 } 1047 } else { 1048 switch (origin) { 1049 case TOP: return 0; 1050 case BASELINE: return -bounds.getMinY(); 1051 case CENTER: return bounds.getHeight() / 2; 1052 case BOTTOM: return bounds.getHeight(); 1053 default: return 0; 1054 } 1055 } 1056 } 1057 1058 /** 1059 * @treatAsPrivate implementation detail 1060 * @deprecated This is an internal API that is not intended 1061 * for use and will be removed in the next version 1062 */ 1063 @Deprecated 1064 @Override 1065 protected final Bounds impl_computeLayoutBounds() { 1066 if (isSpan()) { 1067 BaseBounds bounds = getSpanBounds(); 1068 double width = bounds.getWidth(); 1069 double height = bounds.getHeight(); 1070 return new BoundingBox(0, 0, width, height); 1071 } 1072 1073 if (getBoundsType() == TextBoundsType.VISUAL) { 1074 /* In Node the layout bounds is computed based in the geom 1075 * bounds and in Shape the geom bounds is computed based 1076 * on the shape (generated here in #configShape()) */ 1077 return super.impl_computeLayoutBounds(); 1078 } 1079 BaseBounds bounds = getLogicalBounds(); 1080 double x = bounds.getMinX() + getX(); 1081 double y = bounds.getMinY() + getY() + getYAdjustment(bounds); 1082 double width = bounds.getWidth(); 1083 double height = bounds.getHeight(); 1084 double wrappingWidth = getWrappingWidth(); 1085 if (wrappingWidth != 0) width = wrappingWidth; 1086 return new BoundingBox(x, y, width, height); 1087 } 1088 1089 /** 1090 * @treatAsPrivate implementation detail 1091 * @deprecated This is an internal API that is not intended 1092 * for use and will be removed in the next version 1093 */ 1094 @Deprecated 1095 @Override 1096 public final BaseBounds impl_computeGeomBounds(BaseBounds bounds, 1097 BaseTransform tx) { 1098 if (isSpan()) { 1099 if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) { 1100 return super.impl_computeGeomBounds(bounds, tx); 1101 } 1102 TextLayout layout = getTextLayout(); 1103 bounds = layout.getBounds(getTextSpan(), bounds); 1104 BaseBounds spanBounds = getSpanBounds(); 1105 float minX = bounds.getMinX() - spanBounds.getMinX(); 1106 float minY = bounds.getMinY() - spanBounds.getMinY(); 1107 float maxX = minX + bounds.getWidth(); 1108 float maxY = minY + bounds.getHeight(); 1109 bounds = bounds.deriveWithNewBounds(minX, minY, 0, maxX, maxY, 0); 1110 return tx.transform(bounds, bounds); 1111 } 1112 1113 if (getBoundsType() == TextBoundsType.VISUAL) { 1114 if (getTextInternal().length() == 0 || ShapeHelper.getMode(this) == NGShape.Mode.EMPTY) { 1115 return bounds.makeEmpty(); 1116 } 1117 if (ShapeHelper.getMode(this) == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) { 1118 /* Optimize for FILL and INNER STROKE: save the cost of shaping each glyph */ 1119 BaseBounds visualBounds = getVisualBounds(); 1120 float x = visualBounds.getMinX() + (float) getX(); 1121 float yadj = getYAdjustment(visualBounds); 1122 float y = visualBounds.getMinY() + yadj + (float) getY(); 1123 bounds.deriveWithNewBounds(x, y, 0, x + visualBounds.getWidth(), 1124 y + visualBounds.getHeight(), 0); 1125 return tx.transform(bounds, bounds); 1126 } else { 1127 /* Let the super class compute the bounds using shape */ 1128 return super.impl_computeGeomBounds(bounds, tx); 1129 } 1130 } 1131 1132 BaseBounds textBounds = getLogicalBounds(); 1133 float x = textBounds.getMinX() + (float)getX(); 1134 float yadj = getYAdjustment(textBounds); 1135 float y = textBounds.getMinY() + yadj + (float)getY(); 1136 float width = textBounds.getWidth(); 1137 float height = textBounds.getHeight(); 1138 float wrappingWidth = (float)getWrappingWidth(); 1139 if (wrappingWidth > width) { 1140 width = wrappingWidth; 1141 } else { 1142 /* The following adjustment is necessary for the text bounds to be 1143 * relative to the same location as the mirrored bounds returned 1144 * by layout.getBounds(). 1145 */ 1146 if (wrappingWidth > 0) { 1147 NodeOrientation orientation = getEffectiveNodeOrientation(); 1148 if (orientation == NodeOrientation.RIGHT_TO_LEFT) { 1149 x -= width - wrappingWidth; 1150 } 1151 } 1152 } 1153 textBounds = new RectBounds(x, y, x + width, y + height); 1154 1155 /* handle stroked text */ 1156 if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) { 1157 bounds = 1158 super.impl_computeGeomBounds(bounds, 1159 BaseTransform.IDENTITY_TRANSFORM); 1160 } else { 1161 TextLayout layout = getTextLayout(); 1162 bounds = layout.getBounds(null, bounds); 1163 x = bounds.getMinX() + (float)getX(); 1164 width = bounds.getWidth(); 1165 bounds = bounds.deriveWithNewBounds(x, y, 0, x + width, y + height, 0); 1166 } 1167 1168 bounds = bounds.deriveWithUnion(textBounds); 1169 return tx.transform(bounds, bounds); 1170 } 1171 1172 /** 1173 * @treatAsPrivate implementation detail 1174 * @deprecated This is an internal API that is not intended 1175 * for use and will be removed in the next version 1176 */ 1177 @Deprecated 1178 @Override 1179 protected final boolean impl_computeContains(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 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 impl_geomChanged(); 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 impl_geomChanged(); 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 impl_geomChanged(); 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() { | 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(); 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 } 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); 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 } 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 /** 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 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 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() { |