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 }