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