/* * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.text; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Orientation; import javafx.geometry.VPos; import javafx.scene.AccessibleAttribute; import javafx.scene.AccessibleRole; import javafx.scene.Node; import javafx.scene.layout.Pane; import javafx.scene.shape.PathElement; import javafx.css.StyleableDoubleProperty; import javafx.css.StyleableObjectProperty; import javafx.css.CssMetaData; import javafx.css.converter.EnumConverter; import javafx.css.converter.SizeConverter; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.scene.text.GlyphList; import com.sun.javafx.scene.text.TextLayout; import com.sun.javafx.scene.text.TextLayoutFactory; import com.sun.javafx.scene.text.TextSpan; import com.sun.javafx.tk.Toolkit; import javafx.css.Styleable; import javafx.css.StyleableProperty; /** * TextFlow is special layout designed to lay out rich text. * It can be used to layout several {@link Text} nodes in a single text flow. * The TextFlow uses the text and the font of each {@link Text} node inside of it * plus it own width and text alignment to determine the location for each child. * A single {@link Text} node can span over several lines due to wrapping and * the visual location of {@link Text} node can differ from the logical location * due to bidi reordering. * *
* Any other Node, rather than Text, will be treated as embedded object in the * text layout. It will be inserted in the content using its preferred width, * height, and baseline offset. * *
* When a {@link Text} node is inside of a TextFlow some its properties are ignored.
* For example, the x and y properties of the {@link Text} node are ignored since
* the location of the node is determined by the parent. Likewise, the wrapping
* width in the {@link Text} node is ignored since the width used for wrapping
* is the TextFlow's width. The value of the pickOnBounds
property
* of a {@link Text} is set to false
when it is laid out by the
* TextFlow. This happens because the content of a single {@link Text} node can
* divided and placed in the different locations on the TextFlow (usually due to
* line breaking and bidi reordering).
*
*
* The wrapping width of the layout is determined by the region's current width. * It can be specified by the application by setting the textflow's preferred * width. If no wrapping is desired, the application can either set the preferred * with to Double.MAX_VALUE or Region.USE_COMPUTED_SIZE. * *
* Paragraphs are separated by {@code '\n'} present in any Text child. * *
* Example of a TextFlow: *
* Text text1 = new Text("Big italic red text");
* text1.setFill(Color.RED);
* text1.setFont(Font.font("Helvetica", FontPosture.ITALIC, 40));
* Text text2 = new Text(" little bold blue text");
* text2.setFill(Color.BLUE);
* text2.setFont(Font.font("Helvetica", FontWeight.BOLD, 10));
* TextFlow textFlow = new TextFlow(text1, text2);
*
*
* * TextFlow lays out each managed child regardless of the child's visible property value; * unmanaged children are ignored for all layout calculations.
* ** TextFlow may be styled with backgrounds and borders using CSS. See * {@link javafx.scene.layout.Region Region} superclass for details.
* **
width | height | |
---|---|---|
minimum | *left/right insets | *top/bottom insets plus the height of the text content |
preferred | *left/right insets plus the width of the text content | *top/bottom insets plus the height of the text content |
maximum | *Double.MAX_VALUE | Double.MAX_VALUE |
* A textflow's unbounded maximum width and height are an indication to the parent that * it may be resized beyond its preferred size to fill whatever space is assigned to it. *
* TextFlow provides properties for setting the size range directly. These * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the * application may set them to other values as needed: *
* textflow.setMaxWidth(500);
*
* Applications may restore the computed values by setting these properties back
* to Region.USE_COMPUTED_SIZE.
* * TextFlow does not clip its content by default, so it is possible that childrens' * bounds may extend outside its own bounds if a child's pref size is larger than * the space textflow has to allocate for it.
* * @since JavaFX 8.0 */ public class TextFlow extends Pane { private TextLayout layout; private boolean needsContent; private boolean inLayout; /** * Creates an empty TextFlow layout. */ public TextFlow() { super(); effectiveNodeOrientationProperty().addListener(observable -> checkOrientation()); setAccessibleRole(AccessibleRole.TEXT); } /** * Creates a TextFlow layout with the given children. * * @param children children. */ public TextFlow(Node... children) { this(); getChildren().addAll(children); } private void checkOrientation() { NodeOrientation orientation = getEffectiveNodeOrientation(); boolean rtl = orientation == NodeOrientation.RIGHT_TO_LEFT; int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR; TextLayout layout = getTextLayout(); if (layout.setDirection(dir)) { requestLayout(); } } /** * Maps local point to index in the content. * * @param point the specified point to be tested * @return a {@code HitInfo} representing the character index found * @since 9 */ public final HitInfo hitTest(javafx.geometry.Point2D point) { if (point != null) { TextLayout layout = getTextLayout(); double x = point.getX()/* - getX()*/; double y = point.getY()/* - getY()/* + getYRendering()*/; TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y); return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(), layoutHit.isLeading(), null/*getText()*/); } else { return null; } } /** * Returns shape of caret in local coordinates. * * @param charIndex the character index for the caret * @param caretBias whether the caret is biased on the leading edge of the character * @return an array of {@code PathElement} which can be used to create a {@code Shape} * @since 9 */ public PathElement[] caretShape(int charIndex, boolean leading) { return getTextLayout().getCaretShape(charIndex, leading, 0, 0); } /** * Returns shape for the range of the text in local coordinates. * * @param start the beginning character index for the range * @param start the end character index (non-inclusive) for the range * @return an array of {@code PathElement} which can be used to create a {@code Shape} * @since 9 */ public final PathElement[] rangeShape(int start, int end) { return getRange(start, end, TextLayout.TYPE_TEXT); } @Override public boolean usesMirroring() { return false; } @Override protected void setWidth(double value) { if (value != getWidth()) { TextLayout layout = getTextLayout(); Insets insets = getInsets(); double left = snapSpaceX(insets.getLeft()); double right = snapSpaceX(insets.getRight()); double width = Math.max(1, value - left - right); layout.setWrapWidth((float)width); super.setWidth(value); } } @Override protected double computePrefWidth(double height) { TextLayout layout = getTextLayout(); layout.setWrapWidth(0); double width = layout.getBounds().getWidth(); Insets insets = getInsets(); double left = snapSpaceX(insets.getLeft()); double right = snapSpaceX(insets.getRight()); double wrappingWidth = Math.max(1, getWidth() - left - right); layout.setWrapWidth((float)wrappingWidth); return left + width + right; } @Override protected double computePrefHeight(double width) { TextLayout layout = getTextLayout(); Insets insets = getInsets(); double left = snapSpaceX(insets.getLeft()); double right = snapSpaceX(insets.getRight()); if (width == USE_COMPUTED_SIZE) { layout.setWrapWidth(0); } else { double wrappingWidth = Math.max(1, width - left - right); layout.setWrapWidth((float)wrappingWidth); } double height = layout.getBounds().getHeight(); double wrappingWidth = Math.max(1, getWidth() - left - right); layout.setWrapWidth((float)wrappingWidth); double top = snapSpaceY(insets.getTop()); double bottom = snapSpaceY(insets.getBottom()); return top + height + bottom; } @Override protected double computeMinHeight(double width) { return computePrefHeight(width); } @Override public void requestLayout() { /* The geometry of text nodes can be changed during layout children. * For that reason it has to call impl_geomChanged() causing * requestLayout() to happen during layoutChildren(). * The inLayout flag prevents this call to cause any extra work. */ if (inLayout) return; /* * There is no need to reset the text layout's content every time * requestLayout() is called. For example, the content needs * to be set when: * children add or removed * children managed state changes * children geomChanged (width/height of embedded node) * children content changes (text/font of text node) * The content does not need to set when: * the width/height changes in the region * the insets changes in the region * * Unfortunately, it is not possible to know what change invoked request * layout. The solution is to always reset the content in the text * layout and rely on it to preserve itself if the new content equals to * the old one. The cost to generate the new content is not avoid. */ needsContent = true; super.requestLayout(); } @Override public Orientation getContentBias() { return Orientation.HORIZONTAL; } @Override protected void layoutChildren() { inLayout = true; Insets insets = getInsets(); double top = snapSpaceY(insets.getTop()); double left = snapSpaceX(insets.getLeft()); GlyphList[] runs = getTextLayout().getRuns(); for (int j = 0; j < runs.length; j++) { GlyphList run = runs[j]; TextSpan span = run.getTextSpan(); if (span instanceof EmbeddedSpan) { Node child = ((EmbeddedSpan)span).getNode(); Point2D location = run.getLocation(); double baselineOffset = -run.getLineBounds().getMinY(); layoutInArea(child, left + location.x, top + location.y, run.getWidth(), run.getHeight(), baselineOffset, null, true, true, HPos.CENTER, VPos.BASELINE); } } List