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 }