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 }