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

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

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2021, 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 javafx.css.converter.BooleanConverter;
import javafx.css.converter.EnumConverter;
import javafx.css.converter.SizeConverter;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.TransformedShape;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.shape.ShapeHelper;
import com.sun.javafx.scene.shape.TextHelper;
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.TextLine;
import com.sun.javafx.scene.text.TextSpan;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.sg.prism.NGShape;
import com.sun.javafx.sg.prism.NGText;
import com.sun.javafx.scene.text.FontHelper;
import com.sun.javafx.tk.Toolkit;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.css.CssMetaData;
import javafx.css.FontCssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.Node;

/**
 * The {@code Text} class defines a node that displays a text.
 *
 * Paragraphs are separated by {@code '\n'} and the text is wrapped on
 * paragraph boundaries.
 *
import javafx.scene.text.*;

Text t = new Text(10, 50, "This is a test");
t.setFont(new Font(20));
*
import javafx.scene.text.*;

Text t = new Text();
text.setFont(new Font(20));
text.setText("First row\nSecond row");
*
import javafx.scene.text.*;

Text t = new Text();
text.setFont(new Font(20));
text.setWrappingWidth(200);
text.setTextAlignment(TextAlignment.JUSTIFY)
text.setText("The quick brown fox jumps over the lazy dog");
* @since JavaFX 2.0 */ @DefaultProperty("text") public class Text extends Shape { static { TextHelper.setTextAccessor(new TextHelper.TextAccessor() { @Override public NGNode doCreatePeer(Node node) { return ((Text) node).doCreatePeer(); } @Override public void doUpdatePeer(Node node) { ((Text) node).doUpdatePeer(); } @Override public Bounds doComputeLayoutBounds(Node node) { return ((Text) node).doComputeLayoutBounds(); } @Override public BaseBounds doComputeGeomBounds(Node node, BaseBounds bounds, BaseTransform tx) { return ((Text) node).doComputeGeomBounds(bounds, tx); } @Override public boolean doComputeContains(Node node, double localX, double localY) { return ((Text) node).doComputeContains(localX, localY); } @Override public void doGeomChanged(Node node) { ((Text) node).doGeomChanged(); } @Override public com.sun.javafx.geom.Shape doConfigShape(Shape shape) { return ((Text) shape).doConfigShape(); } }); } private TextLayout layout; private static final PathElement[] EMPTY_PATH_ELEMENT_ARRAY = new PathElement[0]; { // To initialize the class helper at the begining each constructor of this class TextHelper.initHelper(this); } /** * Creates an empty instance of Text. */ public Text() { setAccessibleRole(AccessibleRole.TEXT); InvalidationListener listener = observable -> checkSpan(); parentProperty().addListener(listener); managedProperty().addListener(listener); effectiveNodeOrientationProperty().addListener(observable -> checkOrientation()); setPickOnBounds(true); } /** * Creates an instance of Text containing the given string. * @param text text to be contained in the instance */ public Text(String text) { this(); setText(text); } /** * Creates an instance of Text on the given coordinates containing the * given string. * @param x the horizontal position of the text * @param y the vertical position of the text * @param text text to be contained in the instance */ public Text(double x, double y, String text) { this(text); setX(x); setY(y); } /* * Note: This method MUST only be called via its accessor method. */ private NGNode doCreatePeer() { return new NGText(); } private boolean isSpan; private boolean isSpan() { return isSpan; } private void checkSpan() { isSpan = isManaged() && getParent() instanceof TextFlow; if (isSpan() && !pickOnBoundsProperty().isBound()) { /* Documented behavior. See class description for TextFlow */ setPickOnBounds(false); } } private void checkOrientation() { if (!isSpan()) { 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)) { needsTextLayout(); } } } @Override public boolean usesMirroring() { return false; } private void needsFullTextLayout() { if (isSpan()) { /* Create new text span every time the font or text changes * so the text layout can see that the content has changed. */ textSpan = null; /* Relies on NodeHelper.geomChanged(this) to request text flow to relayout */ } else { TextLayout layout = getTextLayout(); String string = getTextInternal(); Object font = getFontInternal(); layout.setContent(string, font); } needsTextLayout(); } private void needsTextLayout() { textRuns = null; NodeHelper.geomChanged(this); NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); } private TextSpan textSpan; TextSpan getTextSpan() { if (textSpan == null) { textSpan = new TextSpan() { @Override public String getText() { return getTextInternal(); } @Override public Object getFont() { return getFontInternal(); } @Override public RectBounds getBounds() { return null; } }; } return textSpan; } private TextLayout getTextLayout() { if (isSpan()) { layout = null; TextFlow parent = (TextFlow)getParent(); return parent.getTextLayout(); } if (layout == null) { TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory(); layout = factory.createLayout(); String string = getTextInternal(); Object font = getFontInternal(); TextAlignment alignment = getTextAlignment(); if (alignment == null) alignment = DEFAULT_TEXT_ALIGNMENT; layout.setContent(string, font); layout.setAlignment(alignment.ordinal()); layout.setLineSpacing((float)getLineSpacing()); layout.setWrapWidth((float)getWrappingWidth()); if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { layout.setDirection(TextLayout.DIRECTION_RTL); } else { layout.setDirection(TextLayout.DIRECTION_LTR); } layout.setTabSize(getTabSize()); } return layout; } private GlyphList[] textRuns = null; private BaseBounds spanBounds = new RectBounds(); /* relative to the textlayout */ private boolean spanBoundsInvalid = true; void layoutSpan(GlyphList[] runs) { TextSpan span = getTextSpan(); int count = 0; for (int i = 0; i < runs.length; i++) { GlyphList run = runs[i]; if (run.getTextSpan() == span) { count++; } } textRuns = new GlyphList[count]; count = 0; for (int i = 0; i < runs.length; i++) { GlyphList run = runs[i]; if (run.getTextSpan() == span) { textRuns[count++] = run; } } spanBoundsInvalid = true; /* Sometimes a property change in the text node will causes layout in * text flow. In this case all the dirty bits are already clear and no * extra work is necessary. Other times the layout is caused by changes * in the text flow object (wrapping width and text alignment for example). * In the second case the dirty bits must be set here using * NodeHelper.geomChanged(this) and NodeHelper.markDirty(). Note that NodeHelper.geomChanged(this) * causes another (undesired) layout request in the parent. * In general this is not a problem because shapes are not resizable and * region objects do not propagate layout changes to the parent. * This is a special case where a shape is resized by the parent during * layoutChildren(). See TextFlow#requestLayout() for information how * text flow deals with this situation. */ NodeHelper.geomChanged(this); NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); } BaseBounds getSpanBounds() { if (spanBoundsInvalid) { GlyphList[] runs = getRuns(); if (runs.length != 0) { float left = Float.POSITIVE_INFINITY; float top = Float.POSITIVE_INFINITY; float right = 0; float bottom = 0; for (int i = 0; i < runs.length; i++) { GlyphList run = runs[i]; com.sun.javafx.geom.Point2D location = run.getLocation(); float width = run.getWidth(); float height = run.getLineBounds().getHeight(); left = Math.min(location.x, left); top = Math.min(location.y, top); right = Math.max(location.x + width, right); bottom = Math.max(location.y + height, bottom); } spanBounds = spanBounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); } else { spanBounds = spanBounds.makeEmpty(); } spanBoundsInvalid = false; } return spanBounds; } private GlyphList[] getRuns() { if (textRuns != null) return textRuns; if (isSpan()) { /* List of run is initialized when the TextFlow layout the children */ getParent().layout(); } else { TextLayout layout = getTextLayout(); textRuns = layout.getRuns(); } return textRuns; } private com.sun.javafx.geom.Shape getShape() { TextLayout layout = getTextLayout(); /* TextLayout has the text shape cached */ int type = TextLayout.TYPE_TEXT; if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH; if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE; TextSpan filter = null; if (isSpan()) { /* Spans are always relative to the top */ type |= TextLayout.TYPE_TOP; filter = getTextSpan(); } else { /* Relative to baseline (first line) * This shape can be translate in the y axis according * to text origin, see ShapeHelper.configShape(). */ type |= TextLayout.TYPE_BASELINE; } return layout.getShape(type, filter); } private BaseBounds getVisualBounds() { if (ShapeHelper.getMode(this) == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) { int type = TextLayout.TYPE_TEXT; if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH; if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE; return getTextLayout().getVisualBounds(type); } else { return getShape().getBounds(); } } private BaseBounds getLogicalBounds() { TextLayout layout = getTextLayout(); /* TextLayout has the bounds cached */ return layout.getBounds(); } /** * Defines text string that is to be displayed. * * @defaultValue empty string */ private StringProperty text; public final void setText(String value) { if (value == null) value = ""; textProperty().set(value); } public final String getText() { return text == null ? "" : text.get(); } private String getTextInternal() { // this might return null in case of bound property String localText = getText(); return localText == null ? "" : localText; } public final StringProperty textProperty() { if (text == null) { text = new StringPropertyBase("") { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "text"; } @Override public void invalidated() { needsFullTextLayout(); setSelectionStart(-1); setSelectionEnd(-1); setCaretPosition(-1); setCaretBias(true); // MH: Functionality copied from store() method, // which was removed. // Wonder what should happen if text is bound // and becomes null? final String value = get(); if ((value == null) && !isBound()) { set(""); } notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); } }; } return text; } /** * Defines the X coordinate of text origin. * * @defaultValue 0 */ private DoubleProperty x; public final void setX(double value) { xProperty().set(value); } public final double getX() { return x == null ? 0.0 : x.get(); } public final DoubleProperty xProperty() { if (x == null) { x = new DoublePropertyBase() { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "x"; } @Override public void invalidated() { NodeHelper.geomChanged(Text.this); } }; } return x; } /** * Defines the Y coordinate of text origin. * * @defaultValue 0 */ private DoubleProperty y; public final void setY(double value) { yProperty().set(value); } public final double getY() { return y == null ? 0.0 : y.get(); } public final DoubleProperty yProperty() { if (y == null) { y = new DoublePropertyBase() { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "y"; } @Override public void invalidated() { NodeHelper.geomChanged(Text.this); } }; } return y; } /** * Defines the font of text. * * @defaultValue Font{} */ private ObjectProperty font; public final void setFont(Font value) { fontProperty().set(value); } public final Font getFont() { return font == null ? Font.getDefault() : font.get(); } /** * Internally used safe version of getFont which never returns null. * * @return the font */ private Object getFontInternal() { Font font = getFont(); if (font == null) font = Font.getDefault(); return FontHelper.getNativeFont(font); } public final ObjectProperty fontProperty() { if (font == null) { font = new StyleableObjectProperty(Font.getDefault()) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "font"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.FONT; } @Override public void invalidated() { needsFullTextLayout(); NodeHelper.markDirty(Text.this, DirtyBits.TEXT_FONT); } }; } return font; } public final void setTextOrigin(VPos value) { textOriginProperty().set(value); } public final VPos getTextOrigin() { if (attributes == null || attributes.textOrigin == null) { return DEFAULT_TEXT_ORIGIN; } return attributes.getTextOrigin(); } /** * Defines the origin of text coordinate system in local coordinates. * Note: in case multiple rows are rendered {@code VPos.BASELINE} and * {@code VPos.TOP} define the origin of the top row while * {@code VPos.BOTTOM} defines the origin of the bottom row. * * @return the origin of text coordinate system in local coordinates * @defaultValue VPos.BASELINE */ public final ObjectProperty textOriginProperty() { return getTextAttribute().textOriginProperty(); } /** * Determines how the bounds of the text node are calculated. * Logical bounds is a more appropriate default for text than * the visual bounds. See {@code TextBoundsType} for more information. * * @defaultValue TextBoundsType.LOGICAL */ private ObjectProperty boundsType; public final void setBoundsType(TextBoundsType value) { boundsTypeProperty().set(value); } public final TextBoundsType getBoundsType() { return boundsType == null ? DEFAULT_BOUNDS_TYPE : boundsTypeProperty().get(); } public final ObjectProperty boundsTypeProperty() { if (boundsType == null) { boundsType = new StyleableObjectProperty(DEFAULT_BOUNDS_TYPE) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "boundsType"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.BOUNDS_TYPE; } @Override public void invalidated() { TextLayout layout = getTextLayout(); int type = 0; if (boundsType.get() == TextBoundsType.LOGICAL_VERTICAL_CENTER) { type |= TextLayout.BOUNDS_CENTER; } if (layout.setBoundsType(type)) { needsTextLayout(); } else { NodeHelper.geomChanged(Text.this); } } }; } return boundsType; } /** * Defines a width constraint for the text in user space coordinates. * The width is measured in pixels (and not glyph or character count). * If the value is {@code > 0} text will be line wrapped as needed * to satisfy this constraint. * * @defaultValue 0 */ private DoubleProperty wrappingWidth; public final void setWrappingWidth(double value) { wrappingWidthProperty().set(value); } public final double getWrappingWidth() { return wrappingWidth == null ? 0 : wrappingWidth.get(); } public final DoubleProperty wrappingWidthProperty() { if (wrappingWidth == null) { wrappingWidth = new DoublePropertyBase() { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "wrappingWidth"; } @Override public void invalidated() { if (!isSpan()) { TextLayout layout = getTextLayout(); if (layout.setWrapWidth((float)get())) { needsTextLayout(); } else { NodeHelper.geomChanged(Text.this); } } } }; } return wrappingWidth; } public final void setUnderline(boolean value) { underlineProperty().set(value); } public final boolean isUnderline() { if (attributes == null || attributes.underline == null) { return DEFAULT_UNDERLINE; } return attributes.isUnderline(); } /** * Defines if each line of text should have a line below it. * * @return if each line of text should have a line below it * @defaultValue false */ public final BooleanProperty underlineProperty() { return getTextAttribute().underlineProperty(); } public final void setStrikethrough(boolean value) { strikethroughProperty().set(value); } public final boolean isStrikethrough() { if (attributes == null || attributes.strikethrough == null) { return DEFAULT_STRIKETHROUGH; } return attributes.isStrikethrough(); } /** * Defines if each line of text should have a line through it. * * @return if each line of text should have a line through it * @defaultValue false */ public final BooleanProperty strikethroughProperty() { return getTextAttribute().strikethroughProperty(); } public final void setTextAlignment(TextAlignment value) { textAlignmentProperty().set(value); } public final TextAlignment getTextAlignment() { if (attributes == null || attributes.textAlignment == null) { return DEFAULT_TEXT_ALIGNMENT; } return attributes.getTextAlignment(); } /** * Defines horizontal text alignment in the bounding box. * * The width of the bounding box is defined by the widest row. * * Note: In the case of a single line of text, where the width of the * node is determined by the width of the text, the alignment setting * has no effect. * * @return the horizontal text alignment in the bounding box * @defaultValue TextAlignment.LEFT */ public final ObjectProperty textAlignmentProperty() { return getTextAttribute().textAlignmentProperty(); } public final void setLineSpacing(double spacing) { lineSpacingProperty().set(spacing); } public final double getLineSpacing() { if (attributes == null || attributes.lineSpacing == null) { return DEFAULT_LINE_SPACING; } return attributes.getLineSpacing(); } /** * Defines the vertical space in pixel between lines. * * @return the vertical space in pixel between lines * @defaultValue 0 * * @since JavaFX 8.0 */ public final DoubleProperty lineSpacingProperty() { return getTextAttribute().lineSpacingProperty(); } @Override public final double getBaselineOffset() { return baselineOffsetProperty().get(); } /** * The 'alphabetic' (or roman) baseline offset from the Text node's * layoutBounds.minY location. * The value typically corresponds to the max ascent of the font. * @return the baseline offset from this text node */ public final ReadOnlyDoubleProperty baselineOffsetProperty() { return getTextAttribute().baselineOffsetProperty(); } /** * Specifies a requested font smoothing type: gray or LCD. *

* Note: LCD mode doesn't apply in numerous cases, such as various * compositing modes, where effects are applied and very large glyphs. * * @defaultValue {@code FontSmoothingType.GRAY} * @since JavaFX 2.1 */ private ObjectProperty fontSmoothingType; public final void setFontSmoothingType(FontSmoothingType value) { fontSmoothingTypeProperty().set(value); } public final FontSmoothingType getFontSmoothingType() { return fontSmoothingType == null ? FontSmoothingType.GRAY : fontSmoothingType.get(); } public final ObjectProperty fontSmoothingTypeProperty() { if (fontSmoothingType == null) { fontSmoothingType = new StyleableObjectProperty (FontSmoothingType.GRAY) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "fontSmoothingType"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.FONT_SMOOTHING_TYPE; } @Override public void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS); NodeHelper.geomChanged(Text.this); } }; } return fontSmoothingType; } /* * Note: This method MUST only be called via its accessor method. */ private void doGeomChanged() { if (attributes != null) { if (attributes.caretBinding != null) { attributes.caretBinding.invalidate(); } if (attributes.selectionBinding != null) { attributes.selectionBinding.invalidate(); } } NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY); } public final PathElement[] getSelectionShape() { return selectionShapeProperty().get(); } /** * The shape of the selection in local coordinates. * * @return the {@code selectionShape} property * * @since 9 */ public final ReadOnlyObjectProperty selectionShapeProperty() { return getTextAttribute().selectionShapeProperty(); } public final void setSelectionStart(int value) { if (value == -1 && (attributes == null || attributes.selectionStart == null)) { return; } selectionStartProperty().set(value); } public final int getSelectionStart() { if (attributes == null || attributes.selectionStart == null) { return DEFAULT_SELECTION_START; } return attributes.getSelectionStart(); } /** * The start index of the selection in the content. * If the value is -1, the selection is unset. * * @return the {@code selectionStart} property * * @defaultValue -1 * * @since 9 */ public final IntegerProperty selectionStartProperty() { return getTextAttribute().selectionStartProperty(); } public final void setSelectionEnd(int value) { if (value == -1 && (attributes == null || attributes.selectionEnd == null)) { return; } selectionEndProperty().set(value); } public final int getSelectionEnd() { if (attributes == null || attributes.selectionEnd == null) { return DEFAULT_SELECTION_END; } return attributes.getSelectionEnd(); } /** * The end index of the selection in the content. * If the value is -1, the selection is unset. * * @return the {@code selectionEnd} property * * @defaultValue -1 * * @since 9 */ public final IntegerProperty selectionEndProperty() { return getTextAttribute().selectionEndProperty(); } /** * The fill color of selected text. * * @return the fill color of selected text * @since 9 */ public final ObjectProperty selectionFillProperty() { return getTextAttribute().selectionFillProperty(); } public final void setSelectionFill(Paint paint) { selectionFillProperty().set(paint); } public final Paint getSelectionFill() { return selectionFillProperty().get(); } public final PathElement[] getCaretShape() { return caretShapeProperty().get(); } /** * The shape of caret, in local coordinates. * * @return the {@code caretShape} property * * @since 9 */ public final ReadOnlyObjectProperty caretShapeProperty() { return getTextAttribute().caretShapeProperty(); } public final void setCaretPosition(int value) { if (value == -1 && (attributes == null || attributes.caretPosition == null)) { return; } caretPositionProperty().set(value); } public final int getCaretPosition() { if (attributes == null || attributes.caretPosition == null) { return DEFAULT_CARET_POSITION; } return attributes.getCaretPosition(); } /** * The caret index in the content. * If the value is -1, the caret is unset. * * @return the {@code caretPosition} property * * @defaultValue -1 * * @since 9 */ public final IntegerProperty caretPositionProperty() { return getTextAttribute().caretPositionProperty(); } public final void setCaretBias(boolean value) { if (value && (attributes == null || attributes.caretBias == null)) { return; } caretBiasProperty().set(value); } public final boolean isCaretBias() { if (attributes == null || attributes.caretBias == null) { return DEFAULT_CARET_BIAS; } return getTextAttribute().isCaretBias(); } /** * The type of caret bias in the content. If {@code true}, the bias is towards the leading character edge, * otherwise, the bias is towards the trailing character edge. * * @return the {@code caretBias} property * * @defaultValue {@code true} * * @since 9 */ public final BooleanProperty caretBiasProperty() { return getTextAttribute().caretBiasProperty(); } /** * 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(Point2D point) { if (point == null) return 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(), getText()); } private PathElement[] getRange(int start, int end, int type) { int length = getTextInternal().length(); if (0 <= start && start < end && end <= length) { TextLayout layout = getTextLayout(); float x = (float)getX(); float y = (float)getY() - getYRendering(); return layout.getRange(start, end, type, x, y); } return EMPTY_PATH_ELEMENT_ARRAY; } /** * Returns the shape for the caret at the given index and bias. * * @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 final PathElement[] caretShape(int charIndex, boolean caretBias) { if (0 <= charIndex && charIndex <= getTextInternal().length()) { float x = (float)getX(); float y = (float)getY() - getYRendering(); return getTextLayout().getCaretShape(charIndex, caretBias, x, y); } else { return null; } } /** * Returns the 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 9 */ public final PathElement[] underlineShape(int start, int end) { return getRange(start, end, TextLayout.TYPE_UNDERLINE); } private float getYAdjustment(BaseBounds bounds) { VPos origin = getTextOrigin(); if (origin == null) origin = DEFAULT_TEXT_ORIGIN; switch (origin) { case TOP: return -bounds.getMinY(); case BASELINE: return 0; case CENTER: return -bounds.getMinY() - bounds.getHeight() / 2; case BOTTOM: return -bounds.getMinY() - bounds.getHeight(); default: return 0; } } private float getYRendering() { if (isSpan()) return 0; /* Always logical for rendering */ BaseBounds bounds = getLogicalBounds(); VPos origin = getTextOrigin(); if (origin == null) origin = DEFAULT_TEXT_ORIGIN; if (getBoundsType() == TextBoundsType.VISUAL) { BaseBounds vBounds = getVisualBounds(); float delta = vBounds.getMinY() - bounds.getMinY(); switch (origin) { case TOP: return delta; case BASELINE: return -vBounds.getMinY() + delta; case CENTER: return vBounds.getHeight() / 2 + delta; case BOTTOM: return vBounds.getHeight() + delta; default: return 0; } } else { switch (origin) { case TOP: return 0; case BASELINE: return -bounds.getMinY(); case CENTER: return bounds.getHeight() / 2; case BOTTOM: return bounds.getHeight(); default: return 0; } } } private Bounds doComputeLayoutBounds() { if (isSpan()) { BaseBounds bounds = getSpanBounds(); double width = bounds.getWidth(); double height = bounds.getHeight(); return new BoundingBox(0, 0, width, height); } if (getBoundsType() == TextBoundsType.VISUAL) { /* In Node the layout bounds is computed based in the geom * bounds and in Shape the geom bounds is computed based * on the shape (generated here in #configShape()) */ return TextHelper.superComputeLayoutBounds(this); } BaseBounds bounds = getLogicalBounds(); double x = bounds.getMinX() + getX(); double y = bounds.getMinY() + getY() + getYAdjustment(bounds); double width = bounds.getWidth(); double height = bounds.getHeight(); double wrappingWidth = getWrappingWidth(); if (wrappingWidth != 0) width = wrappingWidth; return new BoundingBox(x, y, width, height); } /* * Note: This method MUST only be called via its accessor method. */ private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { if (isSpan()) { if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) { return TextHelper.superComputeGeomBounds(this, bounds, tx); } TextLayout layout = getTextLayout(); bounds = layout.getBounds(getTextSpan(), bounds); BaseBounds spanBounds = getSpanBounds(); float minX = bounds.getMinX() - spanBounds.getMinX(); float minY = bounds.getMinY() - spanBounds.getMinY(); float maxX = minX + bounds.getWidth(); float maxY = minY + bounds.getHeight(); bounds = bounds.deriveWithNewBounds(minX, minY, 0, maxX, maxY, 0); return tx.transform(bounds, bounds); } if (getBoundsType() == TextBoundsType.VISUAL) { if (getTextInternal().length() == 0 || ShapeHelper.getMode(this) == NGShape.Mode.EMPTY) { return bounds.makeEmpty(); } if (ShapeHelper.getMode(this) == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) { /* Optimize for FILL and INNER STROKE: save the cost of shaping each glyph */ BaseBounds visualBounds = getVisualBounds(); float x = visualBounds.getMinX() + (float) getX(); float yadj = getYAdjustment(visualBounds); float y = visualBounds.getMinY() + yadj + (float) getY(); bounds.deriveWithNewBounds(x, y, 0, x + visualBounds.getWidth(), y + visualBounds.getHeight(), 0); return tx.transform(bounds, bounds); } else { /* Let the superclass compute the bounds using shape */ return TextHelper.superComputeGeomBounds(this, bounds, tx); } } BaseBounds textBounds = getLogicalBounds(); float x = textBounds.getMinX() + (float)getX(); float yadj = getYAdjustment(textBounds); float y = textBounds.getMinY() + yadj + (float)getY(); float width = textBounds.getWidth(); float height = textBounds.getHeight(); float wrappingWidth = (float)getWrappingWidth(); if (wrappingWidth > width) { width = wrappingWidth; } else { /* The following adjustment is necessary for the text bounds to be * relative to the same location as the mirrored bounds returned * by layout.getBounds(). */ if (wrappingWidth > 0) { NodeOrientation orientation = getEffectiveNodeOrientation(); if (orientation == NodeOrientation.RIGHT_TO_LEFT) { x -= width - wrappingWidth; } } } textBounds = new RectBounds(x, y, x + width, y + height); /* handle stroked text */ if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) { bounds = TextHelper.superComputeGeomBounds(this, bounds, BaseTransform.IDENTITY_TRANSFORM); } else { TextLayout layout = getTextLayout(); bounds = layout.getBounds(null, bounds); x = bounds.getMinX() + (float)getX(); width = bounds.getWidth(); bounds = bounds.deriveWithNewBounds(x, y, 0, x + width, y + height, 0); } bounds = bounds.deriveWithUnion(textBounds); return tx.transform(bounds, bounds); } /* * Note: This method MUST only be called via its accessor method. */ private boolean doComputeContains(double localX, double localY) { /* Used for spans, regular text uses bounds based picking */ double x = localX + getSpanBounds().getMinX(); double y = localY + getSpanBounds().getMinY(); GlyphList[] runs = getRuns(); if (runs.length != 0) { for (int i = 0; i < runs.length; i++) { GlyphList run = runs[i]; com.sun.javafx.geom.Point2D location = run.getLocation(); float width = run.getWidth(); RectBounds lineBounds = run.getLineBounds(); float height = lineBounds.getHeight(); if (location.x <= x && x < location.x + width && location.y <= y && y < location.y + height) { return true; } } } return false; } /* * Note: This method MUST only be called via its accessor method. */ private com.sun.javafx.geom.Shape doConfigShape() { if (ShapeHelper.getMode(this) == NGShape.Mode.EMPTY || getTextInternal().length() == 0) { return new Path2D(); } com.sun.javafx.geom.Shape shape = getShape(); float x, y; if (isSpan()) { BaseBounds bounds = getSpanBounds(); x = -bounds.getMinX(); y = -bounds.getMinY(); } else { x = (float)getX(); y = getYAdjustment(getVisualBounds()) + (float)getY(); } return TransformedShape.translatedShape(shape, x, y); } /** * The size of a tab stop in spaces. * Values less than 1 are treated as 1. * * @return the {@code tabSize} property * * @defaultValue 8 * * @since 14 */ public final IntegerProperty tabSizeProperty() { return getTextAttribute().tabSizeProperty(); } public final int getTabSize() { if (attributes == null || attributes.tabSize == null) { return TextLayout.DEFAULT_TAB_SIZE; } return getTextAttribute().getTabSize(); } public final void setTabSize(int spaces) { tabSizeProperty().set(spaces); } /* ************************************************************************* * * * Stylesheet Handling * * * **************************************************************************/ /* * Super-lazy instantiation pattern from Bill Pugh. */ private static class StyleableProperties { private static final CssMetaData FONT = new FontCssMetaData("-fx-font", Font.getDefault()) { @Override public boolean isSettable(Text node) { return node.font == null || !node.font.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.fontProperty(); } }; private static final CssMetaData UNDERLINE = new CssMetaData("-fx-underline", BooleanConverter.getInstance(), Boolean.FALSE) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.underline == null || !node.attributes.underline.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.underlineProperty(); } }; private static final CssMetaData STRIKETHROUGH = new CssMetaData("-fx-strikethrough", BooleanConverter.getInstance(), Boolean.FALSE) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.strikethrough == null || !node.attributes.strikethrough.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.strikethroughProperty(); } }; private static final CssMetaData TEXT_ALIGNMENT = new CssMetaData("-fx-text-alignment", new EnumConverter(TextAlignment.class), TextAlignment.LEFT) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.textAlignment == null || !node.attributes.textAlignment.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.textAlignmentProperty(); } }; private static final CssMetaData TEXT_ORIGIN = new CssMetaData("-fx-text-origin", new EnumConverter(VPos.class), VPos.BASELINE) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.textOrigin == null || !node.attributes.textOrigin.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.textOriginProperty(); } }; private static final CssMetaData FONT_SMOOTHING_TYPE = new CssMetaData( "-fx-font-smoothing-type", new EnumConverter(FontSmoothingType.class), FontSmoothingType.GRAY) { @Override public boolean isSettable(Text node) { return node.fontSmoothingType == null || !node.fontSmoothingType.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.fontSmoothingTypeProperty(); } }; private static final CssMetaData LINE_SPACING = new CssMetaData("-fx-line-spacing", SizeConverter.getInstance(), 0) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.lineSpacing == null || !node.attributes.lineSpacing.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.lineSpacingProperty(); } }; private static final CssMetaData BOUNDS_TYPE = new CssMetaData( "-fx-bounds-type", new EnumConverter(TextBoundsType.class), DEFAULT_BOUNDS_TYPE) { @Override public boolean isSettable(Text node) { return node.boundsType == null || !node.boundsType.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.boundsTypeProperty(); } }; private static final CssMetaData TAB_SIZE = new CssMetaData("-fx-tab-size", SizeConverter.getInstance(), TextLayout.DEFAULT_TAB_SIZE) { @Override public boolean isSettable(Text node) { return node.attributes == null || node.attributes.tabSize == null || !node.attributes.tabSize.isBound(); } @Override public StyleableProperty getStyleableProperty(Text node) { return (StyleableProperty)node.tabSizeProperty(); } }; private final static List> STYLEABLES; static { final List> styleables = new ArrayList>(Shape.getClassCssMetaData()); styleables.add(FONT); styleables.add(UNDERLINE); styleables.add(STRIKETHROUGH); styleables.add(TEXT_ALIGNMENT); styleables.add(TEXT_ORIGIN); styleables.add(FONT_SMOOTHING_TYPE); styleables.add(LINE_SPACING); styleables.add(BOUNDS_TYPE); styleables.add(TAB_SIZE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its superclasses. * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * * @since JavaFX 8.0 */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } @SuppressWarnings("deprecation") private void updatePGText() { final NGText peer = NodeHelper.getPeer(this); if (NodeHelper.isDirty(this, DirtyBits.TEXT_ATTRS)) { peer.setUnderline(isUnderline()); peer.setStrikethrough(isStrikethrough()); FontSmoothingType smoothing = getFontSmoothingType(); if (smoothing == null) smoothing = FontSmoothingType.GRAY; peer.setFontSmoothingType(smoothing.ordinal()); } if (NodeHelper.isDirty(this, DirtyBits.TEXT_FONT)) { peer.setFont(getFontInternal()); } if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { peer.setGlyphs(getRuns()); } if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) { if (isSpan()) { BaseBounds spanBounds = getSpanBounds(); peer.setLayoutLocation(spanBounds.getMinX(), spanBounds.getMinY()); } else { float x = (float)getX(); float y = (float)getY(); float yadj = getYRendering(); peer.setLayoutLocation(-x, yadj - y); } } if (NodeHelper.isDirty(this, DirtyBits.TEXT_SELECTION)) { Object fillObj = null; int start = getSelectionStart(); int end = getSelectionEnd(); int length = getTextInternal().length(); if (0 <= start && start < end && end <= length) { Paint fill = selectionFillProperty().get(); fillObj = fill != null ? Toolkit.getPaintAccessor().getPlatformPaint(fill) : null; } peer.setSelection(start, end, fillObj); } } /* * Note: This method MUST only be called via its accessor method. */ private void doUpdatePeer() { updatePGText(); } /* ************************************************************************* * * * Seldom Used Properties * * * **************************************************************************/ private TextAttribute attributes; private TextAttribute getTextAttribute() { if (attributes == null) { attributes = new TextAttribute(); } return attributes; } private static final VPos DEFAULT_TEXT_ORIGIN = VPos.BASELINE; private static final TextBoundsType DEFAULT_BOUNDS_TYPE = TextBoundsType.LOGICAL; private static final boolean DEFAULT_UNDERLINE = false; private static final boolean DEFAULT_STRIKETHROUGH = false; private static final TextAlignment DEFAULT_TEXT_ALIGNMENT = TextAlignment.LEFT; private static final double DEFAULT_LINE_SPACING = 0; private static final int DEFAULT_CARET_POSITION = -1; private static final int DEFAULT_SELECTION_START = -1; private static final int DEFAULT_SELECTION_END = -1; private static final Color DEFAULT_SELECTION_FILL= Color.WHITE; private static final boolean DEFAULT_CARET_BIAS = true; private final class TextAttribute { private ObjectProperty textOrigin; final VPos getTextOrigin() { return textOrigin == null ? DEFAULT_TEXT_ORIGIN : textOrigin.get(); } public final ObjectProperty textOriginProperty() { if (textOrigin == null) { textOrigin = new StyleableObjectProperty(DEFAULT_TEXT_ORIGIN) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "textOrigin"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TEXT_ORIGIN; } @Override public void invalidated() { NodeHelper.geomChanged(Text.this); } }; } return textOrigin; } private BooleanProperty underline; final boolean isUnderline() { return underline == null ? DEFAULT_UNDERLINE : underline.get(); } final BooleanProperty underlineProperty() { if (underline == null) { underline = new StyleableBooleanProperty() { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "underline"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.UNDERLINE; } @Override public void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS); if (getBoundsType() == TextBoundsType.VISUAL) { NodeHelper.geomChanged(Text.this); } } }; } return underline; } private BooleanProperty strikethrough; final boolean isStrikethrough() { return strikethrough == null ? DEFAULT_STRIKETHROUGH : strikethrough.get(); } final BooleanProperty strikethroughProperty() { if (strikethrough == null) { strikethrough = new StyleableBooleanProperty() { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "strikethrough"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.STRIKETHROUGH; } @Override public void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS); if (getBoundsType() == TextBoundsType.VISUAL) { NodeHelper.geomChanged(Text.this); } } }; } return strikethrough; } private ObjectProperty textAlignment; final TextAlignment getTextAlignment() { return textAlignment == null ? DEFAULT_TEXT_ALIGNMENT : textAlignment.get(); } final ObjectProperty textAlignmentProperty() { if (textAlignment == null) { textAlignment = new StyleableObjectProperty(DEFAULT_TEXT_ALIGNMENT) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "textAlignment"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TEXT_ALIGNMENT; } @Override public void invalidated() { if (!isSpan()) { TextAlignment alignment = get(); if (alignment == null) { alignment = DEFAULT_TEXT_ALIGNMENT; } TextLayout layout = getTextLayout(); if (layout.setAlignment(alignment.ordinal())) { needsTextLayout(); } } } }; } return textAlignment; } private DoubleProperty lineSpacing; final double getLineSpacing() { return lineSpacing == null ? DEFAULT_LINE_SPACING : lineSpacing.get(); } final DoubleProperty lineSpacingProperty() { if (lineSpacing == null) { lineSpacing = new StyleableDoubleProperty(DEFAULT_LINE_SPACING) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "lineSpacing"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.LINE_SPACING; } @Override public void invalidated() { if (!isSpan()) { TextLayout layout = getTextLayout(); if (layout.setLineSpacing((float)get())) { needsTextLayout(); } } } }; } return lineSpacing; } private ReadOnlyDoubleWrapper baselineOffset; final ReadOnlyDoubleProperty baselineOffsetProperty() { if (baselineOffset == null) { baselineOffset = new ReadOnlyDoubleWrapper(Text.this, "baselineOffset") { {bind(new DoubleBinding() { {bind(fontProperty());} @Override protected double computeValue() { /* This method should never be used for spans. * If it is, it will still returns the ascent * for the first line in the layout */ BaseBounds bounds = getLogicalBounds(); return -bounds.getMinY(); } });} }; } return baselineOffset.getReadOnlyProperty(); } private ObjectProperty selectionShape; private ObjectBinding selectionBinding; final ReadOnlyObjectProperty selectionShapeProperty() { if (selectionShape == null) { selectionBinding = new ObjectBinding() { {bind(selectionStartProperty(), selectionEndProperty());} @Override protected PathElement[] computeValue() { int start = getSelectionStart(); int end = getSelectionEnd(); return getRange(start, end, TextLayout.TYPE_TEXT); } }; selectionShape = new SimpleObjectProperty(Text.this, "selectionShape"); selectionShape.bind(selectionBinding); } return selectionShape; } private ObjectProperty selectionFill; final ObjectProperty selectionFillProperty() { if (selectionFill == null) { selectionFill = new ObjectPropertyBase(DEFAULT_SELECTION_FILL) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "selectionFill"; } @Override protected void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION); } }; } return selectionFill; } private IntegerProperty selectionStart; final int getSelectionStart() { return selectionStart == null ? DEFAULT_SELECTION_START : selectionStart.get(); } final IntegerProperty selectionStartProperty() { if (selectionStart == null) { selectionStart = new IntegerPropertyBase(DEFAULT_SELECTION_START) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "selectionStart"; } @Override protected void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION); notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_START); } }; } return selectionStart; } private IntegerProperty selectionEnd; final int getSelectionEnd() { return selectionEnd == null ? DEFAULT_SELECTION_END : selectionEnd.get(); } final IntegerProperty selectionEndProperty() { if (selectionEnd == null) { selectionEnd = new IntegerPropertyBase(DEFAULT_SELECTION_END) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "selectionEnd"; } @Override protected void invalidated() { NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION); notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END); } }; } return selectionEnd; } private ObjectProperty caretShape; private ObjectBinding caretBinding; final ReadOnlyObjectProperty caretShapeProperty() { if (caretShape == null) { caretBinding = new ObjectBinding() { {bind(caretPositionProperty(), caretBiasProperty());} @Override protected PathElement[] computeValue() { int pos = getCaretPosition(); int length = getTextInternal().length(); if (0 <= pos && pos <= length) { boolean bias = isCaretBias(); float x = (float)getX(); float y = (float)getY() - getYRendering(); TextLayout layout = getTextLayout(); return layout.getCaretShape(pos, bias, x, y); } return EMPTY_PATH_ELEMENT_ARRAY; } }; caretShape = new SimpleObjectProperty(Text.this, "caretShape"); caretShape.bind(caretBinding); } return caretShape; } private IntegerProperty caretPosition; final int getCaretPosition() { return caretPosition == null ? DEFAULT_CARET_POSITION : caretPosition.get(); } final IntegerProperty caretPositionProperty() { if (caretPosition == null) { caretPosition = new IntegerPropertyBase(DEFAULT_CARET_POSITION) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "caretPosition"; } @Override protected void invalidated() { notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END); } }; } return caretPosition; } private BooleanProperty caretBias; final boolean isCaretBias() { return caretBias == null ? DEFAULT_CARET_BIAS : caretBias.get(); } final BooleanProperty caretBiasProperty() { if (caretBias == null) { caretBias = new SimpleBooleanProperty(Text.this, "caretBias", DEFAULT_CARET_BIAS); } return caretBias; } private IntegerProperty tabSize; final int getTabSize() { return tabSize == null ? TextLayout.DEFAULT_TAB_SIZE : tabSize.get(); } final IntegerProperty tabSizeProperty() { if (tabSize == null) { tabSize = new StyleableIntegerProperty(TextLayout.DEFAULT_TAB_SIZE) { @Override public Object getBean() { return Text.this; } @Override public String getName() { return "tabSize"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TAB_SIZE; } @Override protected void invalidated() { if (!isSpan()) { TextLayout layout = getTextLayout(); if (layout.setTabSize(get())) { needsTextLayout(); } NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS); if (getBoundsType() == TextBoundsType.VISUAL) { NodeHelper.geomChanged(Text.this); } } } }; } return tabSize; } } /** * Returns a string representation of this {@code Text} object. * @return a string representation of this {@code Text} object. */ @Override public String toString() { final StringBuilder sb = new StringBuilder("Text["); String id = getId(); if (id != null) { sb.append("id=").append(id).append(", "); } sb.append("text=\"").append(getText()).append("\""); sb.append(", x=").append(getX()); sb.append(", y=").append(getY()); sb.append(", alignment=").append(getTextAlignment()); sb.append(", origin=").append(getTextOrigin()); sb.append(", boundsType=").append(getBoundsType()); double spacing = getLineSpacing(); if (spacing != DEFAULT_LINE_SPACING) { sb.append(", lineSpacing=").append(spacing); } double wrap = getWrappingWidth(); if (wrap != 0) { sb.append(", wrappingWidth=").append(wrap); } int tab = getTabSize(); if (tab != TextLayout.DEFAULT_TAB_SIZE) { sb.append(", tabSize=").append(tab); } sb.append(", font=").append(getFont()); sb.append(", fontSmoothingType=").append(getFontSmoothingType()); if (isStrikethrough()) { sb.append(", strikethrough"); } if (isUnderline()) { sb.append(", underline"); } sb.append(", fill=").append(getFill()); Paint stroke = getStroke(); if (stroke != null) { sb.append(", stroke=").append(stroke); sb.append(", strokeWidth=").append(getStrokeWidth()); } return sb.append("]").toString(); } /** {@inheritDoc} */ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case TEXT: { String accText = getAccessibleText(); if (accText != null && !accText.isEmpty()) return accText; return getText(); } case FONT: return getFont(); case CARET_OFFSET: { int sel = getCaretPosition(); if (sel >= 0) return sel; return getText().length(); } case SELECTION_START: { int sel = getSelectionStart(); if (sel >= 0) return sel; sel = getCaretPosition(); if (sel >= 0) return sel; return getText().length(); } case SELECTION_END: { int sel = getSelectionEnd(); if (sel >= 0) return sel; sel = getCaretPosition(); if (sel >= 0) return sel; return getText().length(); } case LINE_FOR_OFFSET: { int offset = (Integer)parameters[0]; if (offset > getTextInternal().length()) return null; TextLine[] lines = getTextLayout().getLines(); int lineIndex = 0; for (int i = 1; i < lines.length; i++) { TextLine line = lines[i]; if (line.getStart() > offset) break; lineIndex++; } return lineIndex; } case LINE_START: { int lineIndex = (Integer)parameters[0]; TextLine[] lines = getTextLayout().getLines(); if (0 <= lineIndex && lineIndex < lines.length) { TextLine line = lines[lineIndex]; return line.getStart(); } return null; } case LINE_END: { int lineIndex = (Integer)parameters[0]; TextLine[] lines = getTextLayout().getLines(); if (0 <= lineIndex && lineIndex < lines.length) { TextLine line = lines[lineIndex]; return line.getStart() + line.getLength(); } return null; } case OFFSET_AT_POINT: { Point2D point = (Point2D)parameters[0]; point = screenToLocal(point); return hitTest(point).getCharIndex(); } case BOUNDS_FOR_RANGE: { int start = (Integer)parameters[0]; int end = (Integer)parameters[1]; PathElement[] elements = rangeShape(start, end + 1); /* Each bounds is defined by a MoveTo (top-left) followed by * 4 LineTo (to top-right, bottom-right, bottom-left, back to top-left). */ Bounds[] bounds = new Bounds[elements.length / 5]; int index = 0; for (int i = 0; i < bounds.length; i++) { MoveTo topLeft = (MoveTo)elements[index]; LineTo topRight = (LineTo)elements[index+1]; LineTo bottomRight = (LineTo)elements[index+2]; BoundingBox b = new BoundingBox(topLeft.getX(), topLeft.getY(), topRight.getX() - topLeft.getX(), bottomRight.getY() - topRight.getY()); bounds[i] = localToScreen(b); index += 5; } return bounds; } default: return super.queryAccessibleAttribute(attribute, parameters); } } }