1 /* 2 * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.text; 27 28 import 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 it 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 other Node, rather than Text, will be treated as 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 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 * </code></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 * <h4>Resizable Range</h4> 114 * 115 * A textflow's parent will resize the textflow within the textflow's range 116 * during layout. By default the textflow computes this range based on its content 117 * as outlined in the tables below. 118 * <p> 119 * <table border="1"> 120 * <tr><td></td><th>width</th><th>height</th></tr> 121 * <tr><th>minimum</th> 122 * <td>left/right insets</td> 123 * <td>top/bottom insets plus the height of the text content</td></tr> 124 * <tr><th>preferred</th> 125 * <td>left/right insets plus the width of the text content</td> 126 * <td>top/bottom insets plus the height of the text content</td></tr> 127 * <tr><th>maximum</th> 128 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 129 * </table> 130 * <p> 131 * A textflow's unbounded maximum width and height are an indication to the parent that 132 * it may be resized beyond its preferred size to fill whatever space is assigned to it. 133 * <p> 134 * TextFlow provides properties for setting the size range directly. These 135 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the 136 * application may set them to other values as needed: 137 * <pre><code> 138 * <b>textflow.setMaxWidth(500);</b> 139 * </code></pre> 140 * Applications may restore the computed values by setting these properties back 141 * to Region.USE_COMPUTED_SIZE. 142 * <p> 143 * TextFlow does not clip its content by default, so it is possible that childrens' 144 * bounds may extend outside its own bounds if a child's pref size is larger than 145 * the space textflow has to allocate for it.</p> 146 * 147 * @since JavaFX 8.0 148 */ 149 public class TextFlow extends Pane { 150 151 private TextLayout layout; 152 private boolean needsContent; 153 private boolean inLayout; 154 155 /** 156 * Creates an empty TextFlow layout. 157 */ 158 public TextFlow() { 159 super(); 160 effectiveNodeOrientationProperty().addListener(observable -> checkOrientation()); 161 setAccessibleRole(AccessibleRole.TEXT); 162 } 163 164 /** 165 * Creates a TextFlow layout with the given children. 166 * 167 * @param children children. 168 */ 169 public TextFlow(Node... children) { 170 this(); 171 getChildren().addAll(children); 172 } 173 174 private void checkOrientation() { 175 NodeOrientation orientation = getEffectiveNodeOrientation(); 176 boolean rtl = orientation == NodeOrientation.RIGHT_TO_LEFT; 177 int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR; 178 TextLayout layout = getTextLayout(); 179 if (layout.setDirection(dir)) { 180 requestLayout(); 181 } 182 } 183 184 /** 185 * Maps local point to index in the content. 186 * 187 * @param point the specified point to be tested 188 * @return a {@code HitInfo} representing the character index found 189 * @since 9 190 */ 191 public final HitInfo hitTest(javafx.geometry.Point2D point) { 192 if (point != null) { 193 TextLayout layout = getTextLayout(); 194 double x = point.getX()/* - getX()*/; 195 double y = point.getY()/* - getY()/* + getYRendering()*/; 196 TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y); 197 return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(), 198 layoutHit.isLeading(), null/*getText()*/); 199 } else { 200 return null; 201 } 202 } 203 204 /** 205 * Returns shape of caret in local coordinates. 206 * 207 * @param charIndex the character index for the caret 208 * @param caretBias whether the caret is biased on the leading edge of the character 209 * @return an array of {@code PathElement} which can be used to create a {@code Shape} 210 * @since 9 211 */ 212 public PathElement[] caretShape(int charIndex, boolean leading) { 213 return getTextLayout().getCaretShape(charIndex, leading, 0, 0); 214 } 215 216 /** 217 * Returns shape for the range of the text in local coordinates. 218 * 219 * @param start the beginning character index for the range 220 * @param start the end character index (non-inclusive) for the range 221 * @return an array of {@code PathElement} which can be used to create a {@code Shape} 222 * @since 9 223 */ 224 public final PathElement[] rangeShape(int start, int end) { 225 return getRange(start, end, TextLayout.TYPE_TEXT); 226 } 227 228 @Override 229 public boolean usesMirroring() { 230 return false; 231 } 232 233 @Override protected void setWidth(double value) { 234 if (value != getWidth()) { 235 TextLayout layout = getTextLayout(); 236 Insets insets = getInsets(); 237 double left = snapSpaceX(insets.getLeft()); 238 double right = snapSpaceX(insets.getRight()); 239 double width = Math.max(1, value - left - right); 240 layout.setWrapWidth((float)width); 241 super.setWidth(value); 242 } 243 } 244 245 @Override protected double computePrefWidth(double height) { 246 TextLayout layout = getTextLayout(); 247 layout.setWrapWidth(0); 248 double width = layout.getBounds().getWidth(); 249 Insets insets = getInsets(); 250 double left = snapSpaceX(insets.getLeft()); 251 double right = snapSpaceX(insets.getRight()); 252 double wrappingWidth = Math.max(1, getWidth() - left - right); 253 layout.setWrapWidth((float)wrappingWidth); 254 return left + width + right; 255 } 256 257 @Override protected double computePrefHeight(double width) { 258 TextLayout layout = getTextLayout(); 259 Insets insets = getInsets(); 260 double left = snapSpaceX(insets.getLeft()); 261 double right = snapSpaceX(insets.getRight()); 262 if (width == USE_COMPUTED_SIZE) { 263 layout.setWrapWidth(0); 264 } else { 265 double wrappingWidth = Math.max(1, width - left - right); 266 layout.setWrapWidth((float)wrappingWidth); 267 } 268 double height = layout.getBounds().getHeight(); 269 double wrappingWidth = Math.max(1, getWidth() - left - right); 270 layout.setWrapWidth((float)wrappingWidth); 271 double top = snapSpaceY(insets.getTop()); 272 double bottom = snapSpaceY(insets.getBottom()); 273 return top + height + bottom; 274 } 275 276 @Override protected double computeMinHeight(double width) { 277 return computePrefHeight(width); 278 } 279 280 @Override public void requestLayout() { 281 /* The geometry of text nodes can be changed during layout children. 282 * For that reason it has to call NodeHelper.geomChanged(this) causing 283 * requestLayout() to happen during layoutChildren(). 284 * The inLayout flag prevents this call to cause any extra work. 285 */ 286 if (inLayout) return; 287 288 /* 289 * There is no need to reset the text layout's content every time 290 * requestLayout() is called. For example, the content needs 291 * to be set when: 292 * children add or removed 293 * children managed state changes 294 * children geomChanged (width/height of embedded node) 295 * children content changes (text/font of text node) 296 * The content does not need to set when: 297 * the width/height changes in the region 298 * the insets changes in the region 299 * 300 * Unfortunately, it is not possible to know what change invoked request 301 * layout. The solution is to always reset the content in the text 302 * layout and rely on it to preserve itself if the new content equals to 303 * the old one. The cost to generate the new content is not avoid. 304 */ 305 needsContent = true; 306 super.requestLayout(); 307 } 308 309 @Override public Orientation getContentBias() { 310 return Orientation.HORIZONTAL; 311 } 312 313 @Override protected void layoutChildren() { 314 inLayout = true; 315 Insets insets = getInsets(); 316 double top = snapSpaceY(insets.getTop()); 317 double left = snapSpaceX(insets.getLeft()); 318 319 GlyphList[] runs = getTextLayout().getRuns(); 320 for (int j = 0; j < runs.length; j++) { 321 GlyphList run = runs[j]; 322 TextSpan span = run.getTextSpan(); 323 if (span instanceof EmbeddedSpan) { 324 Node child = ((EmbeddedSpan)span).getNode(); 325 Point2D location = run.getLocation(); 326 double baselineOffset = -run.getLineBounds().getMinY(); 327 328 layoutInArea(child, left + location.x, top + location.y, 329 run.getWidth(), run.getHeight(), 330 baselineOffset, null, true, true, 331 HPos.CENTER, VPos.BASELINE); 332 } 333 } 334 335 List<Node> managed = getManagedChildren(); 336 for (Node node: managed) { 337 if (node instanceof Text) { 338 Text text = (Text)node; 339 text.layoutSpan(runs); 340 BaseBounds spanBounds = text.getSpanBounds(); 341 text.relocate(left + spanBounds.getMinX(), 342 top + spanBounds.getMinY()); 343 } 344 } 345 inLayout = false; 346 } 347 348 private PathElement[] getRange(int start, int end, int type) { 349 TextLayout layout = getTextLayout(); 350 return layout.getRange(start, end, type, 0, 0); 351 } 352 353 private static class EmbeddedSpan implements TextSpan { 354 RectBounds bounds; 355 Node node; 356 public EmbeddedSpan(Node node, double baseline, double width, double height) { 357 this.node = node; 358 bounds = new RectBounds(0, (float)-baseline, 359 (float)width, (float)(height - baseline)); 360 } 361 362 @Override public String getText() { 363 return "\uFFFC"; 364 } 365 366 @Override public Object getFont() { 367 return null; 368 } 369 370 @Override public RectBounds getBounds() { 371 return bounds; 372 } 373 374 public Node getNode() { 375 return node; 376 } 377 } 378 379 TextLayout getTextLayout() { 380 if (layout == null) { 381 TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory(); 382 layout = factory.createLayout(); 383 needsContent = true; 384 } 385 if (needsContent) { 386 List<Node> children = getManagedChildren(); 387 TextSpan[] spans = new TextSpan[children.size()]; 388 for (int i = 0; i < spans.length; i++) { 389 Node node = children.get(i); 390 if (node instanceof Text) { 391 spans[i] = ((Text)node).getTextSpan(); 392 } else { 393 /* Creating a text span every time forces text layout 394 * to run a full text analysis in the new content. 395 */ 396 double baseline = node.getBaselineOffset(); 397 if (baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { 398 baseline = node.getLayoutBounds().getHeight(); 399 } 400 double width = computeChildPrefAreaWidth(node, null); 401 double height = computeChildPrefAreaHeight(node, null); 402 spans[i] = new EmbeddedSpan(node, baseline, width, height); 403 } 404 } 405 layout.setContent(spans); 406 needsContent = false; 407 } 408 return layout; 409 } 410 411 /** 412 * Defines horizontal text alignment. 413 * 414 * @defaultValue TextAlignment.LEFT 415 */ 416 private ObjectProperty<TextAlignment> textAlignment; 417 418 public final void setTextAlignment(TextAlignment value) { 419 textAlignmentProperty().set(value); 420 } 421 422 public final TextAlignment getTextAlignment() { 423 return textAlignment == null ? TextAlignment.LEFT : textAlignment.get(); 424 } 425 426 public final ObjectProperty<TextAlignment> textAlignmentProperty() { 427 if (textAlignment == null) { 428 textAlignment = 429 new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) { 430 @Override public Object getBean() { return TextFlow.this; } 431 @Override public String getName() { return "textAlignment"; } 432 @Override public CssMetaData<TextFlow, TextAlignment> getCssMetaData() { 433 return StyleableProperties.TEXT_ALIGNMENT; 434 } 435 @Override public void invalidated() { 436 TextAlignment align = get(); 437 if (align == null) align = TextAlignment.LEFT; 438 TextLayout layout = getTextLayout(); 439 layout.setAlignment(align.ordinal()); 440 requestLayout(); 441 } 442 }; 443 } 444 return textAlignment; 445 } 446 447 /** 448 * Defines the vertical space in pixel between lines. 449 * 450 * @defaultValue 0 451 * 452 * @since JavaFX 8.0 453 */ 454 private DoubleProperty lineSpacing; 455 456 public final void setLineSpacing(double spacing) { 457 lineSpacingProperty().set(spacing); 458 } 459 460 public final double getLineSpacing() { 461 return lineSpacing == null ? 0 : lineSpacing.get(); 462 } 463 464 public final DoubleProperty lineSpacingProperty() { 465 if (lineSpacing == null) { 466 lineSpacing = 467 new StyleableDoubleProperty(0) { 468 @Override public Object getBean() { return TextFlow.this; } 469 @Override public String getName() { return "lineSpacing"; } 470 @Override public CssMetaData<TextFlow, Number> getCssMetaData() { 471 return StyleableProperties.LINE_SPACING; 472 } 473 @Override public void invalidated() { 474 TextLayout layout = getTextLayout(); 475 if (layout.setLineSpacing((float)get())) { 476 requestLayout(); 477 } 478 } 479 }; 480 } 481 return lineSpacing; 482 } 483 484 @Override public final double getBaselineOffset() { 485 Insets insets = getInsets(); 486 double top = snapSpaceY(insets.getTop()); 487 return top - getTextLayout().getBounds().getMinY(); 488 } 489 490 /*************************************************************************** 491 * * 492 * Stylesheet Handling * 493 * * 494 **************************************************************************/ 495 496 /* 497 * Super-lazy instantiation pattern from Bill Pugh. 498 */ 499 private static class StyleableProperties { 500 501 private static final 502 CssMetaData<TextFlow, TextAlignment> TEXT_ALIGNMENT = 503 new CssMetaData<TextFlow,TextAlignment>("-fx-text-alignment", 504 new EnumConverter<TextAlignment>(TextAlignment.class), 505 TextAlignment.LEFT) { 506 507 @Override public boolean isSettable(TextFlow node) { 508 return node.textAlignment == null || !node.textAlignment.isBound(); 509 } 510 511 @Override public StyleableProperty<TextAlignment> getStyleableProperty(TextFlow node) { 512 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty(); 513 } 514 }; 515 516 private static final 517 CssMetaData<TextFlow,Number> LINE_SPACING = 518 new CssMetaData<TextFlow,Number>("-fx-line-spacing", 519 SizeConverter.getInstance(), 0) { 520 521 @Override public boolean isSettable(TextFlow node) { 522 return node.lineSpacing == null || !node.lineSpacing.isBound(); 523 } 524 525 @Override public StyleableProperty<Number> getStyleableProperty(TextFlow node) { 526 return (StyleableProperty<Number>)node.lineSpacingProperty(); 527 } 528 }; 529 530 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 531 static { 532 final List<CssMetaData<? extends Styleable, ?>> styleables = 533 new ArrayList<CssMetaData<? extends Styleable, ?>>(Pane.getClassCssMetaData()); 534 styleables.add(TEXT_ALIGNMENT); 535 styleables.add(LINE_SPACING); 536 STYLEABLES = Collections.unmodifiableList(styleables); 537 } 538 } 539 540 /** 541 * @return The CssMetaData associated with this class, which may include the 542 * CssMetaData of its super classes. 543 */ 544 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 545 return StyleableProperties.STYLEABLES; 546 } 547 548 @Override 549 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 550 return getClassCssMetaData(); 551 } 552 553 /* The methods in this section are copied from Region due to package visibility restriction */ 554 private static double snapSpace(double value, boolean snapToPixel) { 555 return snapToPixel ? Math.round(value) : value; 556 } 557 558 static double boundedSize(double min, double pref, double max) { 559 double a = pref >= min ? pref : min; 560 double b = min >= max ? min : max; 561 return a <= b ? a : b; 562 } 563 564 double computeChildPrefAreaWidth(Node child, Insets margin) { 565 return computeChildPrefAreaWidth(child, margin, -1); 566 } 567 568 double computeChildPrefAreaWidth(Node child, Insets margin, double height) { 569 final boolean snap = isSnapToPixel(); 570 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 571 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 572 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 573 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 574 double alt = -1; 575 if (child.getContentBias() == Orientation.VERTICAL) { // width depends on height 576 alt = snapSizeY(boundedSize( 577 child.minHeight(-1), height != -1? height - top - bottom : 578 child.prefHeight(-1), child.maxHeight(-1))); 579 } 580 return left + snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; 581 } 582 583 double computeChildPrefAreaHeight(Node child, Insets margin) { 584 return computeChildPrefAreaHeight(child, margin, -1); 585 } 586 587 double computeChildPrefAreaHeight(Node child, Insets margin, double width) { 588 final boolean snap = isSnapToPixel(); 589 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 590 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 591 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 592 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 593 double alt = -1; 594 if (child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width 595 alt = snapSizeX(boundedSize( 596 child.minWidth(-1), width != -1? width - left - right : 597 child.prefWidth(-1), child.maxWidth(-1))); 598 } 599 return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; 600 } 601 /* end of copied code */ 602 603 @Override 604 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 605 switch (attribute) { 606 case TEXT: { 607 String accText = getAccessibleText(); 608 if (accText != null && !accText.isEmpty()) return accText; 609 610 StringBuilder title = new StringBuilder(); 611 for (Node node: getChildren()) { 612 Object text = node.queryAccessibleAttribute(AccessibleAttribute.TEXT, parameters); 613 if (text != null) { 614 title.append(text.toString()); 615 } 616 } 617 return title.toString(); 618 } 619 default: return super.queryAccessibleAttribute(attribute, parameters); 620 } 621 } 622 }