1 /* 2 * Copyright (c) 2012, 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 java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 import javafx.beans.property.DoubleProperty; 32 import javafx.beans.property.ObjectProperty; 33 import javafx.geometry.HPos; 34 import javafx.geometry.Insets; 35 import javafx.geometry.NodeOrientation; 36 import javafx.geometry.Orientation; 37 import javafx.geometry.VPos; 38 import javafx.scene.AccessibleAttribute; 39 import javafx.scene.AccessibleRole; 40 import javafx.scene.Node; 41 import javafx.scene.layout.Pane; 42 import javafx.scene.shape.PathElement; 43 import javafx.css.StyleableDoubleProperty; 44 import javafx.css.StyleableObjectProperty; 45 import javafx.css.CssMetaData; 46 import javafx.css.converter.EnumConverter; 47 import javafx.css.converter.SizeConverter; 48 import com.sun.javafx.geom.BaseBounds; 49 import com.sun.javafx.geom.Point2D; 50 import com.sun.javafx.geom.RectBounds; 51 import com.sun.javafx.scene.text.GlyphList; 52 import com.sun.javafx.scene.text.TextLayout; 53 import com.sun.javafx.scene.text.TextLayoutFactory; 54 import com.sun.javafx.scene.text.TextSpan; 55 import com.sun.javafx.tk.Toolkit; 56 import javafx.css.Styleable; 57 import javafx.css.StyleableProperty; 58 59 /** 60 * TextFlow is special layout designed to lay out rich text. 61 * It can be used to layout several {@link Text} nodes in a single text flow. 62 * The TextFlow uses the text and the font of each {@link Text} node inside of it 63 * plus its own width and text alignment to determine the location for each child. 64 * A single {@link Text} node can span over several lines due to wrapping, and 65 * the visual location of {@link Text} node can differ from the logical location 66 * due to bidi reordering. 67 * 68 * <p> 69 * Any Node, other than Text, will be treated as an embedded object in the 70 * text layout. It will be inserted in the content using its preferred width, 71 * height, and baseline offset. 72 * 73 * <p> 74 * When a {@link Text} node is inside of a TextFlow, some of its properties are ignored. 75 * For example, the x and y properties of the {@link Text} node are ignored since 76 * the location of the node is determined by the parent. Likewise, the wrapping 77 * width in the {@link Text} node is ignored since the width used for wrapping 78 * is the TextFlow's width. The value of the <code>pickOnBounds</code> property 79 * of a {@link Text} is set to <code>false</code> when it is laid out by the 80 * TextFlow. This happens because the content of a single {@link Text} node can 81 * divided and placed in the different locations on the TextFlow (usually due to 82 * line breaking and bidi reordering). 83 * 84 * <p> 85 * The wrapping width of the layout is determined by the region's current width. 86 * It can be specified by the application by setting the textflow's preferred 87 * width. If no wrapping is desired, the application can either set the preferred 88 * with to Double.MAX_VALUE or Region.USE_COMPUTED_SIZE. 89 * 90 * <p> 91 * Paragraphs are separated by {@code '\n'} present in any Text child. 92 * 93 * <p> 94 * Example of a TextFlow: 95 * <pre>{@code 96 * Text text1 = new Text("Big italic red text"); 97 * text1.setFill(Color.RED); 98 * text1.setFont(Font.font("Helvetica", FontPosture.ITALIC, 40)); 99 * Text text2 = new Text(" little bold blue text"); 100 * text2.setFill(Color.BLUE); 101 * text2.setFont(Font.font("Helvetica", FontWeight.BOLD, 10)); 102 * TextFlow textFlow = new TextFlow(text1, text2); 103 * }</pre> 104 * 105 * <p> 106 * TextFlow lays out each managed child regardless of the child's visible property value; 107 * unmanaged children are ignored for all layout calculations.</p> 108 * 109 * <p> 110 * TextFlow may be styled with backgrounds and borders using CSS. See 111 * {@link javafx.scene.layout.Region Region} superclass for details.</p> 112 * 113 * <h2>Resizable Range</h2> 114 * 115 * <p> 116 * A textflow's parent will resize the textflow within the textflow's range 117 * during layout. By default the textflow computes this range based on its content 118 * as outlined in the tables below. 119 * </p> 120 * 121 * <table border="1"> 122 * <caption>TextFlow Resize Table</caption> 123 * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr> 124 * <tr><th scope="row">minimum</th> 125 * <td>left/right insets</td> 126 * <td>top/bottom insets plus the height of the text content</td></tr> 127 * <tr><th scope="row">preferred</th> 128 * <td>left/right insets plus the width of the text content</td> 129 * <td>top/bottom insets plus the height of the text content</td></tr> 130 * <tr><th scope="row">maximum</th> 131 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 132 * </table> 133 * <p> 134 * A textflow's unbounded maximum width and height are an indication to the parent that 135 * it may be resized beyond its preferred size to fill whatever space is assigned to it. 136 * <p> 137 * TextFlow provides properties for setting the size range directly. These 138 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the 139 * application may set them to other values as needed: 140 * <pre><code> 141 * <b>textflow.setMaxWidth(500);</b> 142 * </code></pre> 143 * Applications may restore the computed values by setting these properties back 144 * to Region.USE_COMPUTED_SIZE. 145 * <p> 146 * TextFlow does not clip its content by default, so it is possible that childrens' 147 * bounds may extend outside its own bounds if a child's pref size is larger than 148 * the space textflow has to allocate for it.</p> 149 * 150 * @since JavaFX 8.0 151 */ 152 public class TextFlow extends Pane { 153 154 private TextLayout layout; 155 private boolean needsContent; 156 private boolean inLayout; 157 158 /** 159 * Creates an empty TextFlow layout. 160 */ 161 public TextFlow() { 162 super(); 163 effectiveNodeOrientationProperty().addListener(observable -> checkOrientation()); 164 setAccessibleRole(AccessibleRole.TEXT); 165 } 166 167 /** 168 * Creates a TextFlow layout with the given children. 169 * 170 * @param children children. 171 */ 172 public TextFlow(Node... children) { 173 this(); 174 getChildren().addAll(children); 175 } 176 177 private void checkOrientation() { 178 NodeOrientation orientation = getEffectiveNodeOrientation(); 179 boolean rtl = orientation == NodeOrientation.RIGHT_TO_LEFT; 180 int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR; 181 TextLayout layout = getTextLayout(); 182 if (layout.setDirection(dir)) { 183 requestLayout(); 184 } 185 } 186 187 /** 188 * Maps local point to index in the content. 189 * 190 * @param point the specified point to be tested 191 * @return a {@code HitInfo} representing the character index found 192 * @since 9 193 */ 194 public final HitInfo hitTest(javafx.geometry.Point2D point) { 195 if (point != null) { 196 TextLayout layout = getTextLayout(); 197 double x = point.getX()/* - getX()*/; 198 double y = point.getY()/* - getY()/* + getYRendering()*/; 199 TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y); 200 return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(), 201 layoutHit.isLeading(), null/*getText()*/); 202 } else { 203 return null; 204 } 205 } 206 207 /** 208 * Returns shape of caret in local coordinates. 209 * 210 * @param charIndex the character index for the caret 211 * @param leading whether the caret is biased on the leading edge of the character 212 * @return an array of {@code PathElement} which can be used to create a {@code Shape} 213 * @since 9 214 */ 215 public PathElement[] caretShape(int charIndex, boolean leading) { 216 return getTextLayout().getCaretShape(charIndex, leading, 0, 0); 217 } 218 219 /** 220 * Returns shape for the range of the text in local coordinates. 221 * 222 * @param start the beginning character index for the range 223 * @param end the end character index (non-inclusive) for the range 224 * @return an array of {@code PathElement} which can be used to create a {@code Shape} 225 * @since 9 226 */ 227 public final PathElement[] rangeShape(int start, int end) { 228 return getRange(start, end, TextLayout.TYPE_TEXT); 229 } 230 231 @Override 232 public boolean usesMirroring() { 233 return false; 234 } 235 236 @Override protected void setWidth(double value) { 237 if (value != getWidth()) { 238 TextLayout layout = getTextLayout(); 239 Insets insets = getInsets(); 240 double left = snapSpaceX(insets.getLeft()); 241 double right = snapSpaceX(insets.getRight()); 242 double width = Math.max(1, value - left - right); 243 layout.setWrapWidth((float)width); 244 super.setWidth(value); 245 } 246 } 247 248 @Override protected double computePrefWidth(double height) { 249 TextLayout layout = getTextLayout(); 250 layout.setWrapWidth(0); 251 double width = layout.getBounds().getWidth(); 252 Insets insets = getInsets(); 253 double left = snapSpaceX(insets.getLeft()); 254 double right = snapSpaceX(insets.getRight()); 255 double wrappingWidth = Math.max(1, getWidth() - left - right); 256 layout.setWrapWidth((float)wrappingWidth); 257 return left + width + right; 258 } 259 260 @Override protected double computePrefHeight(double width) { 261 TextLayout layout = getTextLayout(); 262 Insets insets = getInsets(); 263 double left = snapSpaceX(insets.getLeft()); 264 double right = snapSpaceX(insets.getRight()); 265 if (width == USE_COMPUTED_SIZE) { 266 layout.setWrapWidth(0); 267 } else { 268 double wrappingWidth = Math.max(1, width - left - right); 269 layout.setWrapWidth((float)wrappingWidth); 270 } 271 double height = layout.getBounds().getHeight(); 272 double wrappingWidth = Math.max(1, getWidth() - left - right); 273 layout.setWrapWidth((float)wrappingWidth); 274 double top = snapSpaceY(insets.getTop()); 275 double bottom = snapSpaceY(insets.getBottom()); 276 return top + height + bottom; 277 } 278 279 @Override protected double computeMinHeight(double width) { 280 return computePrefHeight(width); 281 } 282 283 @Override public void requestLayout() { 284 /* The geometry of text nodes can be changed during layout children. 285 * For that reason it has to call NodeHelper.geomChanged(this) causing 286 * requestLayout() to happen during layoutChildren(). 287 * The inLayout flag prevents this call to cause any extra work. 288 */ 289 if (inLayout) return; 290 291 /* 292 * There is no need to reset the text layout's content every time 293 * requestLayout() is called. For example, the content needs 294 * to be set when: 295 * children add or removed 296 * children managed state changes 297 * children geomChanged (width/height of embedded node) 298 * children content changes (text/font of text node) 299 * The content does not need to set when: 300 * the width/height changes in the region 301 * the insets changes in the region 302 * 303 * Unfortunately, it is not possible to know what change invoked request 304 * layout. The solution is to always reset the content in the text 305 * layout and rely on it to preserve itself if the new content equals to 306 * the old one. The cost to generate the new content is not avoid. 307 */ 308 needsContent = true; 309 super.requestLayout(); 310 } 311 312 @Override public Orientation getContentBias() { 313 return Orientation.HORIZONTAL; 314 } 315 316 @Override protected void layoutChildren() { 317 inLayout = true; 318 Insets insets = getInsets(); 319 double top = snapSpaceY(insets.getTop()); 320 double left = snapSpaceX(insets.getLeft()); 321 322 GlyphList[] runs = getTextLayout().getRuns(); 323 for (int j = 0; j < runs.length; j++) { 324 GlyphList run = runs[j]; 325 TextSpan span = run.getTextSpan(); 326 if (span instanceof EmbeddedSpan) { 327 Node child = ((EmbeddedSpan)span).getNode(); 328 Point2D location = run.getLocation(); 329 double baselineOffset = -run.getLineBounds().getMinY(); 330 331 layoutInArea(child, left + location.x, top + location.y, 332 run.getWidth(), run.getHeight(), 333 baselineOffset, null, true, true, 334 HPos.CENTER, VPos.BASELINE); 335 } 336 } 337 338 List<Node> managed = getManagedChildren(); 339 for (Node node: managed) { 340 if (node instanceof Text) { 341 Text text = (Text)node; 342 text.layoutSpan(runs); 343 BaseBounds spanBounds = text.getSpanBounds(); 344 text.relocate(left + spanBounds.getMinX(), 345 top + spanBounds.getMinY()); 346 } 347 } 348 inLayout = false; 349 } 350 351 private PathElement[] getRange(int start, int end, int type) { 352 TextLayout layout = getTextLayout(); 353 return layout.getRange(start, end, type, 0, 0); 354 } 355 356 private static class EmbeddedSpan implements TextSpan { 357 RectBounds bounds; 358 Node node; 359 public EmbeddedSpan(Node node, double baseline, double width, double height) { 360 this.node = node; 361 bounds = new RectBounds(0, (float)-baseline, 362 (float)width, (float)(height - baseline)); 363 } 364 365 @Override public String getText() { 366 return "\uFFFC"; 367 } 368 369 @Override public Object getFont() { 370 return null; 371 } 372 373 @Override public RectBounds getBounds() { 374 return bounds; 375 } 376 377 public Node getNode() { 378 return node; 379 } 380 } 381 382 TextLayout getTextLayout() { 383 if (layout == null) { 384 TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory(); 385 layout = factory.createLayout(); 386 needsContent = true; 387 } 388 if (needsContent) { 389 List<Node> children = getManagedChildren(); 390 TextSpan[] spans = new TextSpan[children.size()]; 391 for (int i = 0; i < spans.length; i++) { 392 Node node = children.get(i); 393 if (node instanceof Text) { 394 spans[i] = ((Text)node).getTextSpan(); 395 } else { 396 /* Creating a text span every time forces text layout 397 * to run a full text analysis in the new content. 398 */ 399 double baseline = node.getBaselineOffset(); 400 if (baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { 401 baseline = node.getLayoutBounds().getHeight(); 402 } 403 double width = computeChildPrefAreaWidth(node, null); 404 double height = computeChildPrefAreaHeight(node, null); 405 spans[i] = new EmbeddedSpan(node, baseline, width, height); 406 } 407 } 408 layout.setContent(spans); 409 needsContent = false; 410 } 411 return layout; 412 } 413 414 /** 415 * Defines horizontal text alignment. 416 * 417 * @defaultValue TextAlignment.LEFT 418 */ 419 private ObjectProperty<TextAlignment> textAlignment; 420 421 public final void setTextAlignment(TextAlignment value) { 422 textAlignmentProperty().set(value); 423 } 424 425 public final TextAlignment getTextAlignment() { 426 return textAlignment == null ? TextAlignment.LEFT : textAlignment.get(); 427 } 428 429 public final ObjectProperty<TextAlignment> textAlignmentProperty() { 430 if (textAlignment == null) { 431 textAlignment = 432 new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) { 433 @Override public Object getBean() { return TextFlow.this; } 434 @Override public String getName() { return "textAlignment"; } 435 @Override public CssMetaData<TextFlow, TextAlignment> getCssMetaData() { 436 return StyleableProperties.TEXT_ALIGNMENT; 437 } 438 @Override public void invalidated() { 439 TextAlignment align = get(); 440 if (align == null) align = TextAlignment.LEFT; 441 TextLayout layout = getTextLayout(); 442 layout.setAlignment(align.ordinal()); 443 requestLayout(); 444 } 445 }; 446 } 447 return textAlignment; 448 } 449 450 /** 451 * Defines the vertical space in pixel between lines. 452 * 453 * @defaultValue 0 454 * 455 * @since JavaFX 8.0 456 */ 457 private DoubleProperty lineSpacing; 458 459 public final void setLineSpacing(double spacing) { 460 lineSpacingProperty().set(spacing); 461 } 462 463 public final double getLineSpacing() { 464 return lineSpacing == null ? 0 : lineSpacing.get(); 465 } 466 467 public final DoubleProperty lineSpacingProperty() { 468 if (lineSpacing == null) { 469 lineSpacing = 470 new StyleableDoubleProperty(0) { 471 @Override public Object getBean() { return TextFlow.this; } 472 @Override public String getName() { return "lineSpacing"; } 473 @Override public CssMetaData<TextFlow, Number> getCssMetaData() { 474 return StyleableProperties.LINE_SPACING; 475 } 476 @Override public void invalidated() { 477 TextLayout layout = getTextLayout(); 478 if (layout.setLineSpacing((float)get())) { 479 requestLayout(); 480 } 481 } 482 }; 483 } 484 return lineSpacing; 485 } 486 487 @Override public final double getBaselineOffset() { 488 Insets insets = getInsets(); 489 double top = snapSpaceY(insets.getTop()); 490 return top - getTextLayout().getBounds().getMinY(); 491 } 492 493 /*************************************************************************** 494 * * 495 * Stylesheet Handling * 496 * * 497 **************************************************************************/ 498 499 /* 500 * Super-lazy instantiation pattern from Bill Pugh. 501 */ 502 private static class StyleableProperties { 503 504 private static final 505 CssMetaData<TextFlow, TextAlignment> TEXT_ALIGNMENT = 506 new CssMetaData<TextFlow,TextAlignment>("-fx-text-alignment", 507 new EnumConverter<TextAlignment>(TextAlignment.class), 508 TextAlignment.LEFT) { 509 510 @Override public boolean isSettable(TextFlow node) { 511 return node.textAlignment == null || !node.textAlignment.isBound(); 512 } 513 514 @Override public StyleableProperty<TextAlignment> getStyleableProperty(TextFlow node) { 515 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty(); 516 } 517 }; 518 519 private static final 520 CssMetaData<TextFlow,Number> LINE_SPACING = 521 new CssMetaData<TextFlow,Number>("-fx-line-spacing", 522 SizeConverter.getInstance(), 0) { 523 524 @Override public boolean isSettable(TextFlow node) { 525 return node.lineSpacing == null || !node.lineSpacing.isBound(); 526 } 527 528 @Override public StyleableProperty<Number> getStyleableProperty(TextFlow node) { 529 return (StyleableProperty<Number>)node.lineSpacingProperty(); 530 } 531 }; 532 533 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 534 static { 535 final List<CssMetaData<? extends Styleable, ?>> styleables = 536 new ArrayList<CssMetaData<? extends Styleable, ?>>(Pane.getClassCssMetaData()); 537 styleables.add(TEXT_ALIGNMENT); 538 styleables.add(LINE_SPACING); 539 STYLEABLES = Collections.unmodifiableList(styleables); 540 } 541 } 542 543 /** 544 * @return The CssMetaData associated with this class, which may include the 545 * CssMetaData of its superclasses. 546 */ 547 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 548 return StyleableProperties.STYLEABLES; 549 } 550 551 @Override 552 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 553 return getClassCssMetaData(); 554 } 555 556 /* The methods in this section are copied from Region due to package visibility restriction */ 557 private static double snapSpace(double value, boolean snapToPixel) { 558 return snapToPixel ? Math.round(value) : value; 559 } 560 561 static double boundedSize(double min, double pref, double max) { 562 double a = pref >= min ? pref : min; 563 double b = min >= max ? min : max; 564 return a <= b ? a : b; 565 } 566 567 double computeChildPrefAreaWidth(Node child, Insets margin) { 568 return computeChildPrefAreaWidth(child, margin, -1); 569 } 570 571 double computeChildPrefAreaWidth(Node child, Insets margin, double height) { 572 final boolean snap = isSnapToPixel(); 573 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 574 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 575 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 576 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 577 double alt = -1; 578 if (child.getContentBias() == Orientation.VERTICAL) { // width depends on height 579 alt = snapSizeY(boundedSize( 580 child.minHeight(-1), height != -1? height - top - bottom : 581 child.prefHeight(-1), child.maxHeight(-1))); 582 } 583 return left + snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; 584 } 585 586 double computeChildPrefAreaHeight(Node child, Insets margin) { 587 return computeChildPrefAreaHeight(child, margin, -1); 588 } 589 590 double computeChildPrefAreaHeight(Node child, Insets margin, double width) { 591 final boolean snap = isSnapToPixel(); 592 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 593 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 594 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 595 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 596 double alt = -1; 597 if (child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width 598 alt = snapSizeX(boundedSize( 599 child.minWidth(-1), width != -1? width - left - right : 600 child.prefWidth(-1), child.maxWidth(-1))); 601 } 602 return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; 603 } 604 /* end of copied code */ 605 606 /** {@inheritDoc} */ 607 @Override 608 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 609 switch (attribute) { 610 case TEXT: { 611 String accText = getAccessibleText(); 612 if (accText != null && !accText.isEmpty()) return accText; 613 614 StringBuilder title = new StringBuilder(); 615 for (Node node: getChildren()) { 616 Object text = node.queryAccessibleAttribute(AccessibleAttribute.TEXT, parameters); 617 if (text != null) { 618 title.append(text.toString()); 619 } 620 } 621 return title.toString(); 622 } 623 default: return super.queryAccessibleAttribute(attribute, parameters); 624 } 625 } 626 }