All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.text.TextFlow Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2012, 2023, 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.beans.property.IntegerProperty;
import javafx.css.Styleable;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableProperty;

/**
 * A specialized layout for rich text.
 * It can be used to lay out several {@link Text} nodes in a single text flow.
 * {@code TextFlow} uses the text and the font of each {@code Text} node inside of it,
 * plus its own width and text alignment to determine the location for each child.
 * A single {@code Text} node can span over several lines due to wrapping, and
 * the visual location of the {@code Text} node can differ from the logical location
 * due to bidi reordering.
 *
 * 

* Any {@code Node} other than {@code Text} will be treated as an embedded object in the * text layout. It will be inserted in the content using its preferred width, * height, and baseline offset. * *

* When a {@code Text} node is inside a {@code TextFlow}, some of its properties are ignored. * For example, the {@code x} and {@code y} properties of the {@code Text} node are ignored since * the location of the node is determined by the parent. Likewise, the wrapping * width in the {@code Text} node is ignored since the width used for wrapping * is the {@code TextFlow}'s width. The value of the {@code pickOnBounds} property * of a {@code Text} node is set to {@code false} when it is laid out by the * {@code TextFlow}. This happens because the content of a single {@code Text} node can be * split and placed in different locations in the {@code 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 {@code TextFlow}'s preferred * width. If no wrapping is desired, the application can either set the preferred * with to {@code Double.MAX_VALUE} or * {@link javafx.scene.layout.Region#USE_COMPUTED_SIZE Region.USE_COMPUTED_SIZE}. * *

* Paragraphs are separated by {@code '\n'} present in any {@code Text} child. * *

* Example of a TextFlow: *

{@code
 *     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);
 * }
* *

* {@code TextFlow} lays out each managed child regardless of the child's visible property value; * unmanaged children are ignored for all layout calculations.

* *

* {@code TextFlow} may be styled with backgrounds and borders using CSS. See its * {@link javafx.scene.layout.Region Region} superclass for details.

* *

Resizable Range

* *

* A {@code TextFlow}'s parent will resize the {@code TextFlow} within the {@code TextFlow}'s range * during layout. By default, the {@code TextFlow} computes this range based on its content * as outlined in the tables below. *

* * * * * * * * * * * * *
TextFlow Resize Table
widthheight
minimumleft/right insetstop/bottom insets plus the height of the text content
preferredleft/right insets plus the width of the text contenttop/bottom insets plus the height of the text content
maximum{@code Double.MAX_VALUE}{@code Double.MAX_VALUE}
*

* A {@code 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. *

* {@code TextFlow} provides properties for setting the size range directly. These * properties default to the sentinel value {@code 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 {@code Region.USE_COMPUTED_SIZE}. *

* {@code TextFlow} does not clip its content by default, so it is possible that children's * bounds may extend outside of its own bounds if a child's preferred size is larger than * the space the {@code 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 {@link HitInfo} 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(); double y = point.getY(); TextLayout.Hit h = layout.getHitInfo((float)x, (float)y, null, 0, 0); return new HitInfo(h.getCharIndex(), h.getInsertionIndex(), h.isLeading()); } else { return null; } } /** * Returns shape of caret in local coordinates. * * @param charIndex the character index for the caret * @param leading 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 end 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); } /** * Returns the shape for the underline in local coordinates. * * @param start the beginning character index for the range * @param end 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 21 */ public final PathElement[] underlineShape(int start, int end) { return getRange(start, end, TextLayout.TYPE_UNDERLINE); } @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 NodeHelper.geomChanged(this) 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 managed = getManagedChildren(); for (Node node: managed) { if (node instanceof Text) { Text text = (Text)node; text.layoutSpan(runs); BaseBounds spanBounds = text.getSpanBounds(); text.relocate(left + spanBounds.getMinX(), top + spanBounds.getMinY()); } } inLayout = false; } private PathElement[] getRange(int start, int end, int type) { TextLayout layout = getTextLayout(); return layout.getRange(start, end, type, 0, 0); } private static class EmbeddedSpan implements TextSpan { RectBounds bounds; Node node; public EmbeddedSpan(Node node, double baseline, double width, double height) { this.node = node; bounds = new RectBounds(0, (float)-baseline, (float)width, (float)(height - baseline)); } @Override public String getText() { return "\uFFFC"; } @Override public Object getFont() { return null; } @Override public RectBounds getBounds() { return bounds; } public Node getNode() { return node; } } TextLayout getTextLayout() { if (layout == null) { TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory(); layout = factory.createLayout(); layout.setTabSize(getTabSize()); needsContent = true; } if (needsContent) { List children = getManagedChildren(); TextSpan[] spans = new TextSpan[children.size()]; for (int i = 0; i < spans.length; i++) { Node node = children.get(i); if (node instanceof Text) { spans[i] = ((Text)node).getTextSpan(); } else { /* Creating a text span every time forces text layout * to run a full text analysis in the new content. */ double baseline = node.getBaselineOffset(); if (baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { baseline = node.getLayoutBounds().getHeight(); } double width = computeChildPrefAreaWidth(node, null); double height = computeChildPrefAreaHeight(node, null); spans[i] = new EmbeddedSpan(node, baseline, width, height); } } layout.setContent(spans); needsContent = false; } return layout; } /** * Defines horizontal text alignment. * * @defaultValue TextAlignment.LEFT */ private ObjectProperty textAlignment; public final void setTextAlignment(TextAlignment value) { textAlignmentProperty().set(value); } public final TextAlignment getTextAlignment() { return textAlignment == null ? TextAlignment.LEFT : textAlignment.get(); } public final ObjectProperty textAlignmentProperty() { if (textAlignment == null) { textAlignment = new StyleableObjectProperty(TextAlignment.LEFT) { @Override public Object getBean() { return TextFlow.this; } @Override public String getName() { return "textAlignment"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TEXT_ALIGNMENT; } @Override public void invalidated() { TextAlignment align = get(); if (align == null) align = TextAlignment.LEFT; TextLayout layout = getTextLayout(); layout.setAlignment(align.ordinal()); requestLayout(); } }; } return textAlignment; } /** * Defines the vertical space in pixel between lines. * * @defaultValue 0 * * @since JavaFX 8.0 */ private DoubleProperty lineSpacing; public final void setLineSpacing(double spacing) { lineSpacingProperty().set(spacing); } public final double getLineSpacing() { return lineSpacing == null ? 0 : lineSpacing.get(); } public final DoubleProperty lineSpacingProperty() { if (lineSpacing == null) { lineSpacing = new StyleableDoubleProperty(0) { @Override public Object getBean() { return TextFlow.this; } @Override public String getName() { return "lineSpacing"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.LINE_SPACING; } @Override public void invalidated() { TextLayout layout = getTextLayout(); if (layout.setLineSpacing((float)get())) { requestLayout(); } } }; } return lineSpacing; } /** * The size of a tab stop in spaces. * Values less than 1 are treated as 1. This value overrides the * {@code tabSize} of contained {@link Text} nodes. * * @defaultValue 8 * * @since 14 */ private IntegerProperty tabSize; public final IntegerProperty tabSizeProperty() { if (tabSize == null) { tabSize = new StyleableIntegerProperty(TextLayout.DEFAULT_TAB_SIZE) { @Override public Object getBean() { return TextFlow.this; } @Override public String getName() { return "tabSize"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TAB_SIZE; } @Override protected void invalidated() { TextLayout layout = getTextLayout(); if (layout.setTabSize(get())) { requestLayout(); } } }; } return tabSize; } public final int getTabSize() { return tabSize == null ? TextLayout.DEFAULT_TAB_SIZE : tabSize.get(); } public final void setTabSize(int spaces) { tabSizeProperty().set(spaces); } @Override public final double getBaselineOffset() { Insets insets = getInsets(); double top = snapSpaceY(insets.getTop()); return top - getTextLayout().getBounds().getMinY(); } /* ************************************************************************* * * * Stylesheet Handling * * * **************************************************************************/ /* * Super-lazy instantiation pattern from Bill Pugh. */ private static class StyleableProperties { private static final CssMetaData TEXT_ALIGNMENT = new CssMetaData<>("-fx-text-alignment", new EnumConverter<>(TextAlignment.class), TextAlignment.LEFT) { @Override public boolean isSettable(TextFlow node) { return node.textAlignment == null || !node.textAlignment.isBound(); } @Override public StyleableProperty getStyleableProperty(TextFlow node) { return (StyleableProperty)node.textAlignmentProperty(); } }; private static final CssMetaData LINE_SPACING = new CssMetaData<>("-fx-line-spacing", SizeConverter.getInstance(), 0) { @Override public boolean isSettable(TextFlow node) { return node.lineSpacing == null || !node.lineSpacing.isBound(); } @Override public StyleableProperty getStyleableProperty(TextFlow node) { return (StyleableProperty)node.lineSpacingProperty(); } }; private static final CssMetaData TAB_SIZE = new CssMetaData<>("-fx-tab-size", SizeConverter.getInstance(), TextLayout.DEFAULT_TAB_SIZE) { @Override public boolean isSettable(TextFlow node) { return node.tabSize == null || !node.tabSize.isBound(); } @Override public StyleableProperty getStyleableProperty(TextFlow node) { return (StyleableProperty)node.tabSizeProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList<>(Pane.getClassCssMetaData()); styleables.add(TEXT_ALIGNMENT); styleables.add(LINE_SPACING); styleables.add(TAB_SIZE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * Gets the {@code CssMetaData} associated with this class, which may include the * {@code CssMetaData} of its superclasses. * @return the {@code CssMetaData} */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } @Override public List> getCssMetaData() { return getClassCssMetaData(); } /* The methods in this section are copied from Region due to package visibility restriction */ static double boundedSize(double min, double pref, double max) { double a = pref >= min ? pref : min; double b = min >= max ? min : max; return a <= b ? a : b; } double computeChildPrefAreaWidth(Node child, Insets margin) { return computeChildPrefAreaWidth(child, margin, -1); } double computeChildPrefAreaWidth(Node child, Insets margin, double height) { double top = margin != null? snapSpaceY(margin.getTop()) : 0; double bottom = margin != null? snapSpaceY(margin.getBottom()) : 0; double left = margin != null? snapSpaceX(margin.getLeft()) : 0; double right = margin != null? snapSpaceX(margin.getRight()) : 0; double alt = -1; if (child.getContentBias() == Orientation.VERTICAL) { // width depends on height alt = snapSizeY(boundedSize( child.minHeight(-1), height != -1? height - top - bottom : child.prefHeight(-1), child.maxHeight(-1))); } return left + snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; } double computeChildPrefAreaHeight(Node child, Insets margin) { return computeChildPrefAreaHeight(child, margin, -1); } double computeChildPrefAreaHeight(Node child, Insets margin, double width) { double top = margin != null? snapSpaceY(margin.getTop()) : 0; double bottom = margin != null? snapSpaceY(margin.getBottom()) : 0; double left = margin != null? snapSpaceX(margin.getLeft()) : 0; double right = margin != null? snapSpaceX(margin.getRight()) : 0; double alt = -1; if (child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width alt = snapSizeX(boundedSize( child.minWidth(-1), width != -1? width - left - right : child.prefWidth(-1), child.maxWidth(-1))); } return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; } /* end of copied code */ /** {@inheritDoc} */ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case TEXT: { String accText = getAccessibleText(); if (accText != null && !accText.isEmpty()) return accText; StringBuilder title = new StringBuilder(); for (Node node: getChildren()) { Object text = node.queryAccessibleAttribute(AccessibleAttribute.TEXT, parameters); if (text != null) { title.append(text.toString()); } } return title.toString(); } default: return super.queryAccessibleAttribute(attribute, parameters); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy