1 /*
2 * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.scene.text;
27
28 import javafx.css.converter.BooleanConverter;
29 import javafx.css.converter.EnumConverter;
30 import javafx.css.converter.SizeConverter;
31 import com.sun.javafx.geom.BaseBounds;
32 import com.sun.javafx.geom.Path2D;
33 import com.sun.javafx.geom.RectBounds;
34 import com.sun.javafx.geom.TransformedShape;
35 import com.sun.javafx.geom.transform.BaseTransform;
36 import com.sun.javafx.scene.DirtyBits;
37 import com.sun.javafx.scene.NodeHelper;
38 import com.sun.javafx.scene.shape.ShapeHelper;
39 import com.sun.javafx.scene.shape.TextHelper;
40 import com.sun.javafx.scene.text.GlyphList;
41 import com.sun.javafx.scene.text.TextLayout;
42 import com.sun.javafx.scene.text.TextLayoutFactory;
43 import com.sun.javafx.scene.text.TextLine;
44 import com.sun.javafx.scene.text.TextSpan;
45 import com.sun.javafx.sg.prism.NGNode;
46 import com.sun.javafx.sg.prism.NGShape;
47 import com.sun.javafx.sg.prism.NGText;
48 import com.sun.javafx.scene.text.FontHelper;
49 import com.sun.javafx.tk.Toolkit;
50 import javafx.beans.DefaultProperty;
51 import javafx.beans.InvalidationListener;
52 import javafx.beans.binding.DoubleBinding;
53 import javafx.beans.binding.ObjectBinding;
54 import javafx.scene.AccessibleAttribute;
55 import javafx.scene.AccessibleRole;
56 import javafx.scene.paint.Color;
57 import javafx.scene.paint.Paint;
58 import javafx.scene.shape.LineTo;
59 import javafx.scene.shape.MoveTo;
60 import javafx.scene.shape.PathElement;
61 import javafx.scene.shape.Shape;
62 import javafx.scene.shape.StrokeType;
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.List;
66 import javafx.beans.property.BooleanProperty;
67 import javafx.beans.property.DoubleProperty;
68 import javafx.beans.property.DoublePropertyBase;
69 import javafx.beans.property.IntegerProperty;
70 import javafx.beans.property.IntegerPropertyBase;
71 import javafx.beans.property.ObjectProperty;
72 import javafx.beans.property.ObjectPropertyBase;
73 import javafx.beans.property.ReadOnlyDoubleProperty;
74 import javafx.beans.property.ReadOnlyDoubleWrapper;
75 import javafx.beans.property.ReadOnlyObjectProperty;
76 import javafx.beans.property.SimpleBooleanProperty;
77 import javafx.beans.property.SimpleObjectProperty;
78 import javafx.beans.property.StringProperty;
79 import javafx.beans.property.StringPropertyBase;
80 import javafx.css.CssMetaData;
81 import javafx.css.FontCssMetaData;
82 import javafx.css.Styleable;
83 import javafx.css.StyleableBooleanProperty;
84 import javafx.css.StyleableDoubleProperty;
85 import javafx.css.StyleableIntegerProperty;
86 import javafx.css.StyleableObjectProperty;
87 import javafx.css.StyleableProperty;
88 import javafx.geometry.BoundingBox;
89 import javafx.geometry.Bounds;
90 import javafx.geometry.NodeOrientation;
91 import javafx.geometry.Point2D;
92 import javafx.geometry.VPos;
93 import javafx.scene.Node;
94
95 /**
96 * The {@code Text} class defines a node that displays a text.
97 *
98 * Paragraphs are separated by {@code '\n'} and the text is wrapped on
99 * paragraph boundaries.
100 *
101 <PRE>
102 import javafx.scene.text.*;
103
104 Text t = new Text(10, 50, "This is a test");
105 t.setFont(new Font(20));
106 </PRE>
107 *
108 <PRE>
109 import javafx.scene.text.*;
110
111 Text t = new Text();
112 text.setFont(new Font(20));
113 text.setText("First row\nSecond row");
114 </PRE>
115 *
116 <PRE>
117 import javafx.scene.text.*;
118
119 Text t = new Text();
120 text.setFont(new Font(20));
121 text.setWrappingWidth(200);
122 text.setTextAlignment(TextAlignment.JUSTIFY)
123 text.setText("The quick brown fox jumps over the lazy dog");
124 </PRE>
125 * @since JavaFX 2.0
126 */
127 @DefaultProperty("text")
128 public class Text extends Shape {
129 static {
130 TextHelper.setTextAccessor(new TextHelper.TextAccessor() {
131 @Override
132 public NGNode doCreatePeer(Node node) {
133 return ((Text) node).doCreatePeer();
134 }
135
136 @Override
137 public void doUpdatePeer(Node node) {
138 ((Text) node).doUpdatePeer();
139 }
140
141 @Override
142 public Bounds doComputeLayoutBounds(Node node) {
143 return ((Text) node).doComputeLayoutBounds();
144 }
145
146 @Override
147 public BaseBounds doComputeGeomBounds(Node node,
148 BaseBounds bounds, BaseTransform tx) {
149 return ((Text) node).doComputeGeomBounds(bounds, tx);
150 }
151
152 @Override
153 public boolean doComputeContains(Node node, double localX, double localY) {
154 return ((Text) node).doComputeContains(localX, localY);
155 }
156
157 @Override
158 public void doGeomChanged(Node node) {
159 ((Text) node).doGeomChanged();
160 }
161
162 @Override
163 public com.sun.javafx.geom.Shape doConfigShape(Shape shape) {
164 return ((Text) shape).doConfigShape();
165 }
166 });
167 }
168
169 private TextLayout layout;
170 private static final PathElement[] EMPTY_PATH_ELEMENT_ARRAY = new PathElement[0];
171
172 {
173 // To initialize the class helper at the begining each constructor of this class
174 TextHelper.initHelper(this);
175 }
176
177 /**
178 * Creates an empty instance of Text.
179 */
180 public Text() {
181 setAccessibleRole(AccessibleRole.TEXT);
182 InvalidationListener listener = observable -> checkSpan();
183 parentProperty().addListener(listener);
184 managedProperty().addListener(listener);
185 effectiveNodeOrientationProperty().addListener(observable -> checkOrientation());
186 setPickOnBounds(true);
187 }
188
189 /**
190 * Creates an instance of Text containing the given string.
191 * @param text text to be contained in the instance
192 */
193 public Text(String text) {
194 this();
195 setText(text);
196 }
197
198 /**
199 * Creates an instance of Text on the given coordinates containing the
200 * given string.
201 * @param x the horizontal position of the text
202 * @param y the vertical position of the text
203 * @param text text to be contained in the instance
204 */
205 public Text(double x, double y, String text) {
206 this(text);
207 setX(x);
208 setY(y);
209 }
210
211 /*
212 * Note: This method MUST only be called via its accessor method.
213 */
214 private NGNode doCreatePeer() {
215 return new NGText();
216 }
217
218 private boolean isSpan;
219 private boolean isSpan() {
220 return isSpan;
221 }
222
223 private void checkSpan() {
224 isSpan = isManaged() && getParent() instanceof TextFlow;
225 if (isSpan() && !pickOnBoundsProperty().isBound()) {
226 /* Documented behavior. See class description for TextFlow */
227 setPickOnBounds(false);
228 }
229 }
230
231 private void checkOrientation() {
232 if (!isSpan()) {
233 NodeOrientation orientation = getEffectiveNodeOrientation();
234 boolean rtl = orientation == NodeOrientation.RIGHT_TO_LEFT;
235 int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR;
236 TextLayout layout = getTextLayout();
237 if (layout.setDirection(dir)) {
238 needsTextLayout();
239 }
240 }
241 }
242
243 @Override
244 public boolean usesMirroring() {
245 return false;
246 }
247
248 private void needsFullTextLayout() {
249 if (isSpan()) {
250 /* Create new text span every time the font or text changes
251 * so the text layout can see that the content has changed.
252 */
253 textSpan = null;
254
255 /* Relies on NodeHelper.geomChanged(this) to request text flow to relayout */
256 } else {
257 TextLayout layout = getTextLayout();
258 String string = getTextInternal();
259 Object font = getFontInternal();
260 layout.setContent(string, font);
261 }
262 needsTextLayout();
263 }
264
265 private void needsTextLayout() {
266 textRuns = null;
267 NodeHelper.geomChanged(this);
268 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS);
269 }
270
271 private TextSpan textSpan;
272 TextSpan getTextSpan() {
273 if (textSpan == null) {
274 textSpan = new TextSpan() {
275 @Override public String getText() {
276 return getTextInternal();
277 }
278 @Override public Object getFont() {
279 return getFontInternal();
280 }
281 @Override public RectBounds getBounds() {
282 return null;
283 }
284 };
285 }
286 return textSpan;
287 }
288
289 private TextLayout getTextLayout() {
290 if (isSpan()) {
291 layout = null;
292 TextFlow parent = (TextFlow)getParent();
293 return parent.getTextLayout();
294 }
295 if (layout == null) {
296 TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory();
297 layout = factory.createLayout();
298 String string = getTextInternal();
299 Object font = getFontInternal();
300 TextAlignment alignment = getTextAlignment();
301 if (alignment == null) alignment = DEFAULT_TEXT_ALIGNMENT;
302 layout.setContent(string, font);
303 layout.setAlignment(alignment.ordinal());
304 layout.setLineSpacing((float)getLineSpacing());
305 layout.setWrapWidth((float)getWrappingWidth());
306 if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
307 layout.setDirection(TextLayout.DIRECTION_RTL);
308 } else {
309 layout.setDirection(TextLayout.DIRECTION_LTR);
310 }
311 layout.setTabSize(getTabSize());
312 }
313 return layout;
314 }
315
316 private GlyphList[] textRuns = null;
317 private BaseBounds spanBounds = new RectBounds(); /* relative to the textlayout */
318 private boolean spanBoundsInvalid = true;
319
320 void layoutSpan(GlyphList[] runs) {
321 TextSpan span = getTextSpan();
322 int count = 0;
323 for (int i = 0; i < runs.length; i++) {
324 GlyphList run = runs[i];
325 if (run.getTextSpan() == span) {
326 count++;
327 }
328 }
329 textRuns = new GlyphList[count];
330 count = 0;
331 for (int i = 0; i < runs.length; i++) {
332 GlyphList run = runs[i];
333 if (run.getTextSpan() == span) {
334 textRuns[count++] = run;
335 }
336 }
337 spanBoundsInvalid = true;
338
339 /* Sometimes a property change in the text node will causes layout in
340 * text flow. In this case all the dirty bits are already clear and no
341 * extra work is necessary. Other times the layout is caused by changes
342 * in the text flow object (wrapping width and text alignment for example).
343 * In the second case the dirty bits must be set here using
344 * NodeHelper.geomChanged(this) and NodeHelper.markDirty(). Note that NodeHelper.geomChanged(this)
345 * causes another (undesired) layout request in the parent.
346 * In general this is not a problem because shapes are not resizable and
347 * region objects do not propagate layout changes to the parent.
348 * This is a special case where a shape is resized by the parent during
349 * layoutChildren(). See TextFlow#requestLayout() for information how
350 * text flow deals with this situation.
351 */
352 NodeHelper.geomChanged(this);
353 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS);
354 }
355
356 BaseBounds getSpanBounds() {
357 if (spanBoundsInvalid) {
358 GlyphList[] runs = getRuns();
359 if (runs.length != 0) {
360 float left = Float.POSITIVE_INFINITY;
361 float top = Float.POSITIVE_INFINITY;
362 float right = 0;
363 float bottom = 0;
364 for (int i = 0; i < runs.length; i++) {
365 GlyphList run = runs[i];
366 com.sun.javafx.geom.Point2D location = run.getLocation();
367 float width = run.getWidth();
368 float height = run.getLineBounds().getHeight();
369 left = Math.min(location.x, left);
370 top = Math.min(location.y, top);
371 right = Math.max(location.x + width, right);
372 bottom = Math.max(location.y + height, bottom);
373 }
374 spanBounds = spanBounds.deriveWithNewBounds(left, top, 0,
375 right, bottom, 0);
376 } else {
377 spanBounds = spanBounds.makeEmpty();
378 }
379 spanBoundsInvalid = false;
380 }
381 return spanBounds;
382 }
383
384 private GlyphList[] getRuns() {
385 if (textRuns != null) return textRuns;
386 if (isSpan()) {
387 /* List of run is initialized when the TextFlow layout the children */
388 getParent().layout();
389 } else {
390 TextLayout layout = getTextLayout();
391 textRuns = layout.getRuns();
392 }
393 return textRuns;
394 }
395
396 private com.sun.javafx.geom.Shape getShape() {
397 TextLayout layout = getTextLayout();
398 /* TextLayout has the text shape cached */
399 int type = TextLayout.TYPE_TEXT;
400 if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH;
401 if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE;
402
403 TextSpan filter = null;
404 if (isSpan()) {
405 /* Spans are always relative to the top */
406 type |= TextLayout.TYPE_TOP;
407 filter = getTextSpan();
408 } else {
409 /* Relative to baseline (first line)
410 * This shape can be translate in the y axis according
411 * to text origin, see ShapeHelper.configShape().
412 */
413 type |= TextLayout.TYPE_BASELINE;
414 }
415 return layout.getShape(type, filter);
416 }
417
418 private BaseBounds getVisualBounds() {
419 if (ShapeHelper.getMode(this) == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) {
420 int type = TextLayout.TYPE_TEXT;
421 if (isStrikethrough()) type |= TextLayout.TYPE_STRIKETHROUGH;
422 if (isUnderline()) type |= TextLayout.TYPE_UNDERLINE;
423 return getTextLayout().getVisualBounds(type);
424 } else {
425 return getShape().getBounds();
426 }
427 }
428
429 private BaseBounds getLogicalBounds() {
430 TextLayout layout = getTextLayout();
431 /* TextLayout has the bounds cached */
432 return layout.getBounds();
433 }
434
435 /**
436 * Defines text string that is to be displayed.
437 *
438 * @defaultValue empty string
439 */
440 private StringProperty text;
441
442 public final void setText(String value) {
443 if (value == null) value = "";
444 textProperty().set(value);
445 }
446
447 public final String getText() {
448 return text == null ? "" : text.get();
449 }
450
451 private String getTextInternal() {
452 // this might return null in case of bound property
453 String localText = getText();
454 return localText == null ? "" : localText;
455 }
456
457 public final StringProperty textProperty() {
458 if (text == null) {
459 text = new StringPropertyBase("") {
460 @Override public Object getBean() { return Text.this; }
461 @Override public String getName() { return "text"; }
462 @Override public void invalidated() {
463 needsFullTextLayout();
464 setSelectionStart(-1);
465 setSelectionEnd(-1);
466 setCaretPosition(-1);
467 setCaretBias(true);
468
469 // MH: Functionality copied from store() method,
470 // which was removed.
471 // Wonder what should happen if text is bound
472 // and becomes null?
473 final String value = get();
474 if ((value == null) && !isBound()) {
475 set("");
476 }
477 notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
478 }
479 };
480 }
481 return text;
482 }
483
484 /**
485 * Defines the X coordinate of text origin.
486 *
487 * @defaultValue 0
488 */
489 private DoubleProperty x;
490
491 public final void setX(double value) {
492 xProperty().set(value);
493 }
494
495 public final double getX() {
496 return x == null ? 0.0 : x.get();
497 }
498
499 public final DoubleProperty xProperty() {
500 if (x == null) {
501 x = new DoublePropertyBase() {
502 @Override public Object getBean() { return Text.this; }
503 @Override public String getName() { return "x"; }
504 @Override public void invalidated() {
505 NodeHelper.geomChanged(Text.this);
506 }
507 };
508 }
509 return x;
510 }
511
512 /**
513 * Defines the Y coordinate of text origin.
514 *
515 * @defaultValue 0
516 */
517 private DoubleProperty y;
518
519 public final void setY(double value) {
520 yProperty().set(value);
521 }
522
523 public final double getY() {
524 return y == null ? 0.0 : y.get();
525 }
526
527 public final DoubleProperty yProperty() {
528 if (y == null) {
529 y = new DoublePropertyBase() {
530 @Override public Object getBean() { return Text.this; }
531 @Override public String getName() { return "y"; }
532 @Override public void invalidated() {
533 NodeHelper.geomChanged(Text.this);
534 }
535 };
536 }
537 return y;
538 }
539
540 /**
541 * Defines the font of text.
542 *
543 * @defaultValue Font{}
544 */
545 private ObjectProperty<Font> font;
546
547 public final void setFont(Font value) {
548 fontProperty().set(value);
549 }
550
551 public final Font getFont() {
552 return font == null ? Font.getDefault() : font.get();
553 }
554
555 /**
556 * Internally used safe version of getFont which never returns null.
557 *
558 * @return the font
559 */
560 private Object getFontInternal() {
561 Font font = getFont();
562 if (font == null) font = Font.getDefault();
563 return FontHelper.getNativeFont(font);
564 }
565
566 public final ObjectProperty<Font> fontProperty() {
567 if (font == null) {
568 font = new StyleableObjectProperty<Font>(Font.getDefault()) {
569 @Override public Object getBean() { return Text.this; }
570 @Override public String getName() { return "font"; }
571 @Override public CssMetaData<Text,Font> getCssMetaData() {
572 return StyleableProperties.FONT;
573 }
574 @Override public void invalidated() {
575 needsFullTextLayout();
576 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_FONT);
577 }
578 };
579 }
580 return font;
581 }
582
583 public final void setTextOrigin(VPos value) {
584 textOriginProperty().set(value);
585 }
586
587 public final VPos getTextOrigin() {
588 if (attributes == null || attributes.textOrigin == null) {
589 return DEFAULT_TEXT_ORIGIN;
590 }
591 return attributes.getTextOrigin();
592 }
593
594 /**
595 * Defines the origin of text coordinate system in local coordinates.
596 * Note: in case multiple rows are rendered {@code VPos.BASELINE} and
597 * {@code VPos.TOP} define the origin of the top row while
598 * {@code VPos.BOTTOM} defines the origin of the bottom row.
599 *
600 * @return the origin of text coordinate system in local coordinates
601 * @defaultValue VPos.BASELINE
602 */
603 public final ObjectProperty<VPos> textOriginProperty() {
604 return getTextAttribute().textOriginProperty();
605 }
606
607 /**
608 * Determines how the bounds of the text node are calculated.
609 * Logical bounds is a more appropriate default for text than
610 * the visual bounds. See {@code TextBoundsType} for more information.
611 *
612 * @defaultValue TextBoundsType.LOGICAL
613 */
614 private ObjectProperty<TextBoundsType> boundsType;
615
616 public final void setBoundsType(TextBoundsType value) {
617 boundsTypeProperty().set(value);
618 }
619
620 public final TextBoundsType getBoundsType() {
621 return boundsType == null ?
622 DEFAULT_BOUNDS_TYPE : boundsTypeProperty().get();
623 }
624
625 public final ObjectProperty<TextBoundsType> boundsTypeProperty() {
626 if (boundsType == null) {
627 boundsType =
628 new StyleableObjectProperty<TextBoundsType>(DEFAULT_BOUNDS_TYPE) {
629 @Override public Object getBean() { return Text.this; }
630 @Override public String getName() { return "boundsType"; }
631 @Override public CssMetaData<Text,TextBoundsType> getCssMetaData() {
632 return StyleableProperties.BOUNDS_TYPE;
633 }
634 @Override public void invalidated() {
635 TextLayout layout = getTextLayout();
636 int type = 0;
637 if (boundsType.get() == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
638 type |= TextLayout.BOUNDS_CENTER;
639 }
640 if (layout.setBoundsType(type)) {
641 needsTextLayout();
642 } else {
643 NodeHelper.geomChanged(Text.this);
644 }
645 }
646 };
647 }
648 return boundsType;
649 }
650
651 /**
652 * Defines a width constraint for the text in user space coordinates.
653 * The width is measured in pixels (and not glyph or character count).
654 * If the value is {@code > 0} text will be line wrapped as needed
655 * to satisfy this constraint.
656 *
657 * @defaultValue 0
658 */
659 private DoubleProperty wrappingWidth;
660
661 public final void setWrappingWidth(double value) {
662 wrappingWidthProperty().set(value);
663 }
664
665 public final double getWrappingWidth() {
666 return wrappingWidth == null ? 0 : wrappingWidth.get();
667 }
668
669 public final DoubleProperty wrappingWidthProperty() {
670 if (wrappingWidth == null) {
671 wrappingWidth = new DoublePropertyBase() {
672 @Override public Object getBean() { return Text.this; }
673 @Override public String getName() { return "wrappingWidth"; }
674 @Override public void invalidated() {
675 if (!isSpan()) {
676 TextLayout layout = getTextLayout();
677 if (layout.setWrapWidth((float)get())) {
678 needsTextLayout();
679 } else {
680 NodeHelper.geomChanged(Text.this);
681 }
682 }
683 }
684 };
685 }
686 return wrappingWidth;
687 }
688
689 public final void setUnderline(boolean value) {
690 underlineProperty().set(value);
691 }
692
693 public final boolean isUnderline() {
694 if (attributes == null || attributes.underline == null) {
695 return DEFAULT_UNDERLINE;
696 }
697 return attributes.isUnderline();
698 }
699
700 /**
701 * Defines if each line of text should have a line below it.
702 *
703 * @return if each line of text should have a line below it
704 * @defaultValue false
705 */
706 public final BooleanProperty underlineProperty() {
707 return getTextAttribute().underlineProperty();
708 }
709
710 public final void setStrikethrough(boolean value) {
711 strikethroughProperty().set(value);
712 }
713
714 public final boolean isStrikethrough() {
715 if (attributes == null || attributes.strikethrough == null) {
716 return DEFAULT_STRIKETHROUGH;
717 }
718 return attributes.isStrikethrough();
719 }
720
721 /**
722 * Defines if each line of text should have a line through it.
723 *
724 * @return if each line of text should have a line through it
725 * @defaultValue false
726 */
727 public final BooleanProperty strikethroughProperty() {
728 return getTextAttribute().strikethroughProperty();
729 }
730
731 public final void setTextAlignment(TextAlignment value) {
732 textAlignmentProperty().set(value);
733 }
734
735 public final TextAlignment getTextAlignment() {
736 if (attributes == null || attributes.textAlignment == null) {
737 return DEFAULT_TEXT_ALIGNMENT;
738 }
739 return attributes.getTextAlignment();
740 }
741
742 /**
743 * Defines horizontal text alignment in the bounding box.
744 *
745 * The width of the bounding box is defined by the widest row.
746 *
747 * Note: In the case of a single line of text, where the width of the
748 * node is determined by the width of the text, the alignment setting
749 * has no effect.
750 *
751 * @return the horizontal text alignment in the bounding box
752 * @defaultValue TextAlignment.LEFT
753 */
754 public final ObjectProperty<TextAlignment> textAlignmentProperty() {
755 return getTextAttribute().textAlignmentProperty();
756 }
757
758 public final void setLineSpacing(double spacing) {
759 lineSpacingProperty().set(spacing);
760 }
761
762 public final double getLineSpacing() {
763 if (attributes == null || attributes.lineSpacing == null) {
764 return DEFAULT_LINE_SPACING;
765 }
766 return attributes.getLineSpacing();
767 }
768
769 /**
770 * Defines the vertical space in pixel between lines.
771 *
772 * @return the vertical space in pixel between lines
773 * @defaultValue 0
774 *
775 * @since JavaFX 8.0
776 */
777 public final DoubleProperty lineSpacingProperty() {
778 return getTextAttribute().lineSpacingProperty();
779 }
780
781 @Override
782 public final double getBaselineOffset() {
783 return baselineOffsetProperty().get();
784 }
785
786 /**
787 * The 'alphabetic' (or roman) baseline offset from the Text node's
788 * layoutBounds.minY location.
789 * The value typically corresponds to the max ascent of the font.
790 * @return the baseline offset from this text node
791 */
792 public final ReadOnlyDoubleProperty baselineOffsetProperty() {
793 return getTextAttribute().baselineOffsetProperty();
794 }
795
796 /**
797 * Specifies a requested font smoothing type: gray or LCD.
798 *
799 * The width of the bounding box is defined by the widest row.
800 *
801 * Note: LCD mode doesn't apply in numerous cases, such as various
802 * compositing modes, where effects are applied and very large glyphs.
803 *
804 * @defaultValue FontSmoothingType.GRAY
805 * @since JavaFX 2.1
806 */
807 private ObjectProperty<FontSmoothingType> fontSmoothingType;
808
809 public final void setFontSmoothingType(FontSmoothingType value) {
810 fontSmoothingTypeProperty().set(value);
811 }
812
813 public final FontSmoothingType getFontSmoothingType() {
814 return fontSmoothingType == null ?
815 FontSmoothingType.GRAY : fontSmoothingType.get();
816 }
817
818 public final ObjectProperty<FontSmoothingType>
819 fontSmoothingTypeProperty() {
820 if (fontSmoothingType == null) {
821 fontSmoothingType =
822 new StyleableObjectProperty<FontSmoothingType>
823 (FontSmoothingType.GRAY) {
824 @Override public Object getBean() { return Text.this; }
825 @Override public String getName() { return "fontSmoothingType"; }
826 @Override public CssMetaData<Text,FontSmoothingType> getCssMetaData() {
827 return StyleableProperties.FONT_SMOOTHING_TYPE;
828 }
829 @Override public void invalidated() {
830 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS);
831 NodeHelper.geomChanged(Text.this);
832 }
833 };
834 }
835 return fontSmoothingType;
836 }
837
838 /*
839 * Note: This method MUST only be called via its accessor method.
840 */
841 private void doGeomChanged() {
842 if (attributes != null) {
843 if (attributes.caretBinding != null) {
844 attributes.caretBinding.invalidate();
845 }
846 if (attributes.selectionBinding != null) {
847 attributes.selectionBinding.invalidate();
848 }
849 }
850 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY);
851 }
852
853 public final PathElement[] getSelectionShape() {
854 return selectionShapeProperty().get();
855 }
856
857 /**
858 * The shape of the selection in local coordinates.
859 *
860 * @return the {@code selectionShape} property
861 *
862 * @since 9
863 */
864 public final ReadOnlyObjectProperty<PathElement[]> selectionShapeProperty() {
865 return getTextAttribute().selectionShapeProperty();
866 }
867
868 public final void setSelectionStart(int value) {
869 if (value == -1 &&
870 (attributes == null || attributes.selectionStart == null)) {
871 return;
872 }
873 selectionStartProperty().set(value);
874 }
875
876 public final int getSelectionStart() {
877 if (attributes == null || attributes.selectionStart == null) {
878 return DEFAULT_SELECTION_START;
879 }
880 return attributes.getSelectionStart();
881 }
882
883 /**
884 * The start index of the selection in the content.
885 * If the value is -1, the selection is unset.
886 *
887 * @return the {@code selectionStart} property
888 *
889 * @defaultValue -1
890 *
891 * @since 9
892 */
893 public final IntegerProperty selectionStartProperty() {
894 return getTextAttribute().selectionStartProperty();
895 }
896
897 public final void setSelectionEnd(int value) {
898 if (value == -1 &&
899 (attributes == null || attributes.selectionEnd == null)) {
900 return;
901 }
902 selectionEndProperty().set(value);
903 }
904
905 public final int getSelectionEnd() {
906 if (attributes == null || attributes.selectionEnd == null) {
907 return DEFAULT_SELECTION_END;
908 }
909 return attributes.getSelectionEnd();
910 }
911
912 /**
913 * The end index of the selection in the content.
914 * If the value is -1, the selection is unset.
915 *
916 * @return the {@code selectionEnd} property
917 *
918 * @defaultValue -1
919 *
920 * @since 9
921 */
922 public final IntegerProperty selectionEndProperty() {
923 return getTextAttribute().selectionEndProperty();
924 }
925
926 /**
927 * The fill color of selected text.
928 *
929 * @return the fill color of selected text
930 * @since 9
931 */
932 public final ObjectProperty<Paint> selectionFillProperty() {
933 return getTextAttribute().selectionFillProperty();
934 }
935
936 public final void setSelectionFill(Paint paint) {
937 selectionFillProperty().set(paint);
938 }
939 public final Paint getSelectionFill() {
940 return selectionFillProperty().get();
941 }
942
943 public final PathElement[] getCaretShape() {
944 return caretShapeProperty().get();
945 }
946
947 /**
948 * The shape of caret, in local coordinates.
949 *
950 * @return the {@code caretShape} property
951 *
952 * @since 9
953 */
954 public final ReadOnlyObjectProperty<PathElement[]> caretShapeProperty() {
955 return getTextAttribute().caretShapeProperty();
956 }
957
958 public final void setCaretPosition(int value) {
959 if (value == -1 &&
960 (attributes == null || attributes.caretPosition == null)) {
961 return;
962 }
963 caretPositionProperty().set(value);
964 }
965
966 public final int getCaretPosition() {
967 if (attributes == null || attributes.caretPosition == null) {
968 return DEFAULT_CARET_POSITION;
969 }
970 return attributes.getCaretPosition();
971 }
972
973 /**
974 * The caret index in the content.
975 * If the value is -1, the caret is unset.
976 *
977 * @return the {@code caretPosition} property
978 *
979 * @defaultValue -1
980 *
981 * @since 9
982 */
983 public final IntegerProperty caretPositionProperty() {
984 return getTextAttribute().caretPositionProperty();
985 }
986
987 public final void setCaretBias(boolean value) {
988 if (value && (attributes == null || attributes.caretBias == null)) {
989 return;
990 }
991 caretBiasProperty().set(value);
992 }
993
994 public final boolean isCaretBias() {
995 if (attributes == null || attributes.caretBias == null) {
996 return DEFAULT_CARET_BIAS;
997 }
998 return getTextAttribute().isCaretBias();
999 }
1000
1001 /**
1002 * The type of caret bias in the content. If {@code true}, the bias is towards the leading character edge,
1003 * otherwise, the bias is towards the trailing character edge.
1004 *
1005 * @return the {@code caretBias} property
1006 *
1007 * @defaultValue {@code true}
1008 *
1009 * @since 9
1010 */
1011 public final BooleanProperty caretBiasProperty() {
1012 return getTextAttribute().caretBiasProperty();
1013 }
1014
1015 /**
1016 * Maps local point to index in the content.
1017 *
1018 * @param point the specified point to be tested
1019 * @return a {@code HitInfo} representing the character index found
1020 * @since 9
1021 */
1022 public final HitInfo hitTest(Point2D point) {
1023 if (point == null) return null;
1024 TextLayout layout = getTextLayout();
1025 double x = point.getX() - getX();
1026 double y = point.getY() - getY() + getYRendering();
1027 TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y);
1028 return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(),
1029 layoutHit.isLeading(), getText());
1030 }
1031
1032 private PathElement[] getRange(int start, int end, int type) {
1033 int length = getTextInternal().length();
1034 if (0 <= start && start < end && end <= length) {
1035 TextLayout layout = getTextLayout();
1036 float x = (float)getX();
1037 float y = (float)getY() - getYRendering();
1038 return layout.getRange(start, end, type, x, y);
1039 }
1040 return EMPTY_PATH_ELEMENT_ARRAY;
1041 }
1042
1043 /**
1044 * Returns the shape for the caret at the given index and bias.
1045 *
1046 * @param charIndex the character index for the caret
1047 * @param caretBias whether the caret is biased on the leading edge of the character
1048 * @return an array of {@code PathElement} which can be used to create a {@code Shape}
1049 * @since 9
1050 */
1051 public final PathElement[] caretShape(int charIndex, boolean caretBias) {
1052 if (0 <= charIndex && charIndex <= getTextInternal().length()) {
1053 float x = (float)getX();
1054 float y = (float)getY() - getYRendering();
1055 return getTextLayout().getCaretShape(charIndex, caretBias, x, y);
1056 } else {
1057 return null;
1058 }
1059 }
1060
1061 /**
1062 * Returns the shape for the range of the text in local coordinates.
1063 *
1064 * @param start the beginning character index for the range
1065 * @param end the end character index (non-inclusive) for the range
1066 * @return an array of {@code PathElement} which can be used to create a {@code Shape}
1067 * @since 9
1068 */
1069 public final PathElement[] rangeShape(int start, int end) {
1070 return getRange(start, end, TextLayout.TYPE_TEXT);
1071 }
1072
1073 /**
1074 * Returns the shape for the underline in local coordinates.
1075 *
1076 * @param start the beginning character index for the range
1077 * @param end the end character index (non-inclusive) for the range
1078 * @return an array of {@code PathElement} which can be used to create a {@code Shape}
1079 * @since 9
1080 */
1081 public final PathElement[] underlineShape(int start, int end) {
1082 return getRange(start, end, TextLayout.TYPE_UNDERLINE);
1083 }
1084
1085 private float getYAdjustment(BaseBounds bounds) {
1086 VPos origin = getTextOrigin();
1087 if (origin == null) origin = DEFAULT_TEXT_ORIGIN;
1088 switch (origin) {
1089 case TOP: return -bounds.getMinY();
1090 case BASELINE: return 0;
1091 case CENTER: return -bounds.getMinY() - bounds.getHeight() / 2;
1092 case BOTTOM: return -bounds.getMinY() - bounds.getHeight();
1093 default: return 0;
1094 }
1095 }
1096
1097 private float getYRendering() {
1098 if (isSpan()) return 0;
1099
1100 /* Always logical for rendering */
1101 BaseBounds bounds = getLogicalBounds();
1102
1103 VPos origin = getTextOrigin();
1104 if (origin == null) origin = DEFAULT_TEXT_ORIGIN;
1105 if (getBoundsType() == TextBoundsType.VISUAL) {
1106 BaseBounds vBounds = getVisualBounds();
1107 float delta = vBounds.getMinY() - bounds.getMinY();
1108 switch (origin) {
1109 case TOP: return delta;
1110 case BASELINE: return -vBounds.getMinY() + delta;
1111 case CENTER: return vBounds.getHeight() / 2 + delta;
1112 case BOTTOM: return vBounds.getHeight() + delta;
1113 default: return 0;
1114 }
1115 } else {
1116 switch (origin) {
1117 case TOP: return 0;
1118 case BASELINE: return -bounds.getMinY();
1119 case CENTER: return bounds.getHeight() / 2;
1120 case BOTTOM: return bounds.getHeight();
1121 default: return 0;
1122 }
1123 }
1124 }
1125
1126 private Bounds doComputeLayoutBounds() {
1127 if (isSpan()) {
1128 BaseBounds bounds = getSpanBounds();
1129 double width = bounds.getWidth();
1130 double height = bounds.getHeight();
1131 return new BoundingBox(0, 0, width, height);
1132 }
1133
1134 if (getBoundsType() == TextBoundsType.VISUAL) {
1135 /* In Node the layout bounds is computed based in the geom
1136 * bounds and in Shape the geom bounds is computed based
1137 * on the shape (generated here in #configShape()) */
1138 return TextHelper.superComputeLayoutBounds(this);
1139 }
1140 BaseBounds bounds = getLogicalBounds();
1141 double x = bounds.getMinX() + getX();
1142 double y = bounds.getMinY() + getY() + getYAdjustment(bounds);
1143 double width = bounds.getWidth();
1144 double height = bounds.getHeight();
1145 double wrappingWidth = getWrappingWidth();
1146 if (wrappingWidth != 0) width = wrappingWidth;
1147 return new BoundingBox(x, y, width, height);
1148 }
1149
1150 /*
1151 * Note: This method MUST only be called via its accessor method.
1152 */
1153 private BaseBounds doComputeGeomBounds(BaseBounds bounds,
1154 BaseTransform tx) {
1155 if (isSpan()) {
1156 if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) {
1157 return TextHelper.superComputeGeomBounds(this, bounds, tx);
1158 }
1159 TextLayout layout = getTextLayout();
1160 bounds = layout.getBounds(getTextSpan(), bounds);
1161 BaseBounds spanBounds = getSpanBounds();
1162 float minX = bounds.getMinX() - spanBounds.getMinX();
1163 float minY = bounds.getMinY() - spanBounds.getMinY();
1164 float maxX = minX + bounds.getWidth();
1165 float maxY = minY + bounds.getHeight();
1166 bounds = bounds.deriveWithNewBounds(minX, minY, 0, maxX, maxY, 0);
1167 return tx.transform(bounds, bounds);
1168 }
1169
1170 if (getBoundsType() == TextBoundsType.VISUAL) {
1171 if (getTextInternal().length() == 0 || ShapeHelper.getMode(this) == NGShape.Mode.EMPTY) {
1172 return bounds.makeEmpty();
1173 }
1174 if (ShapeHelper.getMode(this) == NGShape.Mode.FILL || getStrokeType() == StrokeType.INSIDE) {
1175 /* Optimize for FILL and INNER STROKE: save the cost of shaping each glyph */
1176 BaseBounds visualBounds = getVisualBounds();
1177 float x = visualBounds.getMinX() + (float) getX();
1178 float yadj = getYAdjustment(visualBounds);
1179 float y = visualBounds.getMinY() + yadj + (float) getY();
1180 bounds.deriveWithNewBounds(x, y, 0, x + visualBounds.getWidth(),
1181 y + visualBounds.getHeight(), 0);
1182 return tx.transform(bounds, bounds);
1183 } else {
1184 /* Let the superclass compute the bounds using shape */
1185 return TextHelper.superComputeGeomBounds(this, bounds, tx);
1186 }
1187 }
1188
1189 BaseBounds textBounds = getLogicalBounds();
1190 float x = textBounds.getMinX() + (float)getX();
1191 float yadj = getYAdjustment(textBounds);
1192 float y = textBounds.getMinY() + yadj + (float)getY();
1193 float width = textBounds.getWidth();
1194 float height = textBounds.getHeight();
1195 float wrappingWidth = (float)getWrappingWidth();
1196 if (wrappingWidth > width) {
1197 width = wrappingWidth;
1198 } else {
1199 /* The following adjustment is necessary for the text bounds to be
1200 * relative to the same location as the mirrored bounds returned
1201 * by layout.getBounds().
1202 */
1203 if (wrappingWidth > 0) {
1204 NodeOrientation orientation = getEffectiveNodeOrientation();
1205 if (orientation == NodeOrientation.RIGHT_TO_LEFT) {
1206 x -= width - wrappingWidth;
1207 }
1208 }
1209 }
1210 textBounds = new RectBounds(x, y, x + width, y + height);
1211
1212 /* handle stroked text */
1213 if (ShapeHelper.getMode(this) != NGShape.Mode.FILL && getStrokeType() != StrokeType.INSIDE) {
1214 bounds = TextHelper.superComputeGeomBounds(this, bounds,
1215 BaseTransform.IDENTITY_TRANSFORM);
1216 } else {
1217 TextLayout layout = getTextLayout();
1218 bounds = layout.getBounds(null, bounds);
1219 x = bounds.getMinX() + (float)getX();
1220 width = bounds.getWidth();
1221 bounds = bounds.deriveWithNewBounds(x, y, 0, x + width, y + height, 0);
1222 }
1223
1224 bounds = bounds.deriveWithUnion(textBounds);
1225 return tx.transform(bounds, bounds);
1226 }
1227
1228 /*
1229 * Note: This method MUST only be called via its accessor method.
1230 */
1231 private boolean doComputeContains(double localX, double localY) {
1232 /* Used for spans, regular text uses bounds based picking */
1233 double x = localX + getSpanBounds().getMinX();
1234 double y = localY + getSpanBounds().getMinY();
1235 GlyphList[] runs = getRuns();
1236 if (runs.length != 0) {
1237 for (int i = 0; i < runs.length; i++) {
1238 GlyphList run = runs[i];
1239 com.sun.javafx.geom.Point2D location = run.getLocation();
1240 float width = run.getWidth();
1241 RectBounds lineBounds = run.getLineBounds();
1242 float height = lineBounds.getHeight();
1243 if (location.x <= x && x < location.x + width &&
1244 location.y <= y && y < location.y + height) {
1245 return true;
1246 }
1247 }
1248 }
1249 return false;
1250 }
1251
1252 /*
1253 * Note: This method MUST only be called via its accessor method.
1254 */
1255 private com.sun.javafx.geom.Shape doConfigShape() {
1256 if (ShapeHelper.getMode(this) == NGShape.Mode.EMPTY || getTextInternal().length() == 0) {
1257 return new Path2D();
1258 }
1259 com.sun.javafx.geom.Shape shape = getShape();
1260 float x, y;
1261 if (isSpan()) {
1262 BaseBounds bounds = getSpanBounds();
1263 x = -bounds.getMinX();
1264 y = -bounds.getMinY();
1265 } else {
1266 x = (float)getX();
1267 y = getYAdjustment(getVisualBounds()) + (float)getY();
1268 }
1269 return TransformedShape.translatedShape(shape, x, y);
1270 }
1271
1272 /**
1273 * The size of a tab stop in spaces.
1274 *
1275 * @return the {@code tabSize} property
1276 *
1277 * @defaultValue {@code 8}
1278 *
1279 * @since 14
1280 */
1281 public final IntegerProperty tabSizeProperty() {
1282 return getTextAttribute().tabSizeProperty();
1283 }
1284
1285 /**
1286 * Gets the size of a tab stop in spaces.
1287 * @return the size of a tab in spaces
1288 * @since 14
1289 */
1290 public final int getTabSize() {
1291 if (attributes == null || attributes.tabSize == null) {
1292 return TextLayout.DEFAULT_TAB_SIZE;
1293 }
1294 return getTextAttribute().getTabSize();
1295 }
1296
1297 /**
1298 * Sets the size of a tab stop.
1299 * @param spaces the size of a tab in spaces. Defaults to 8.
1300 * Minimum is 1, lower values will be clamped to 1.
1301 * @since 14
1302 */
1303 public final void setTabSize(int spaces) {
1304 tabSizeProperty().set(spaces);
1305 }
1306
1307
1308 /***************************************************************************
1309 * *
1310 * Stylesheet Handling *
1311 * *
1312 **************************************************************************/
1313
1314 /*
1315 * Super-lazy instantiation pattern from Bill Pugh.
1316 */
1317 private static class StyleableProperties {
1318
1319 private static final CssMetaData<Text,Font> FONT =
1320 new FontCssMetaData<Text>("-fx-font", Font.getDefault()) {
1321
1322 @Override
1323 public boolean isSettable(Text node) {
1324 return node.font == null || !node.font.isBound();
1325 }
1326
1327 @Override
1328 public StyleableProperty<Font> getStyleableProperty(Text node) {
1329 return (StyleableProperty<Font>)node.fontProperty();
1330 }
1331 };
1332
1333 private static final CssMetaData<Text,Boolean> UNDERLINE =
1334 new CssMetaData<Text,Boolean>("-fx-underline",
1335 BooleanConverter.getInstance(), Boolean.FALSE) {
1336
1337 @Override
1338 public boolean isSettable(Text node) {
1339 return node.attributes == null ||
1340 node.attributes.underline == null ||
1341 !node.attributes.underline.isBound();
1342 }
1343
1344 @Override
1345 public StyleableProperty<Boolean> getStyleableProperty(Text node) {
1346 return (StyleableProperty<Boolean>)node.underlineProperty();
1347 }
1348 };
1349
1350 private static final CssMetaData<Text,Boolean> STRIKETHROUGH =
1351 new CssMetaData<Text,Boolean>("-fx-strikethrough",
1352 BooleanConverter.getInstance(), Boolean.FALSE) {
1353
1354 @Override
1355 public boolean isSettable(Text node) {
1356 return node.attributes == null ||
1357 node.attributes.strikethrough == null ||
1358 !node.attributes.strikethrough.isBound();
1359 }
1360
1361 @Override
1362 public StyleableProperty<Boolean> getStyleableProperty(Text node) {
1363 return (StyleableProperty<Boolean>)node.strikethroughProperty();
1364 }
1365 };
1366
1367 private static final
1368 CssMetaData<Text,TextAlignment> TEXT_ALIGNMENT =
1369 new CssMetaData<Text,TextAlignment>("-fx-text-alignment",
1370 new EnumConverter<TextAlignment>(TextAlignment.class),
1371 TextAlignment.LEFT) {
1372
1373 @Override
1374 public boolean isSettable(Text node) {
1375 return node.attributes == null ||
1376 node.attributes.textAlignment == null ||
1377 !node.attributes.textAlignment.isBound();
1378 }
1379
1380 @Override
1381 public StyleableProperty<TextAlignment> getStyleableProperty(Text node) {
1382 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty();
1383 }
1384 };
1385
1386 private static final CssMetaData<Text,VPos> TEXT_ORIGIN =
1387 new CssMetaData<Text,VPos>("-fx-text-origin",
1388 new EnumConverter<VPos>(VPos.class),
1389 VPos.BASELINE) {
1390
1391 @Override
1392 public boolean isSettable(Text node) {
1393 return node.attributes == null ||
1394 node.attributes.textOrigin == null ||
1395 !node.attributes.textOrigin.isBound();
1396 }
1397
1398 @Override
1399 public StyleableProperty<VPos> getStyleableProperty(Text node) {
1400 return (StyleableProperty<VPos>)node.textOriginProperty();
1401 }
1402 };
1403
1404 private static final CssMetaData<Text,FontSmoothingType>
1405 FONT_SMOOTHING_TYPE =
1406 new CssMetaData<Text,FontSmoothingType>(
1407 "-fx-font-smoothing-type",
1408 new EnumConverter<FontSmoothingType>(FontSmoothingType.class),
1409 FontSmoothingType.GRAY) {
1410
1411 @Override
1412 public boolean isSettable(Text node) {
1413 return node.fontSmoothingType == null ||
1414 !node.fontSmoothingType.isBound();
1415 }
1416
1417 @Override
1418 public StyleableProperty<FontSmoothingType>
1419 getStyleableProperty(Text node) {
1420
1421 return (StyleableProperty<FontSmoothingType>)node.fontSmoothingTypeProperty();
1422 }
1423 };
1424
1425 private static final
1426 CssMetaData<Text,Number> LINE_SPACING =
1427 new CssMetaData<Text,Number>("-fx-line-spacing",
1428 SizeConverter.getInstance(), 0) {
1429
1430 @Override
1431 public boolean isSettable(Text node) {
1432 return node.attributes == null ||
1433 node.attributes.lineSpacing == null ||
1434 !node.attributes.lineSpacing.isBound();
1435 }
1436
1437 @Override
1438 public StyleableProperty<Number> getStyleableProperty(Text node) {
1439 return (StyleableProperty<Number>)node.lineSpacingProperty();
1440 }
1441 };
1442
1443 private static final CssMetaData<Text, TextBoundsType>
1444 BOUNDS_TYPE =
1445 new CssMetaData<Text,TextBoundsType>(
1446 "-fx-bounds-type",
1447 new EnumConverter<TextBoundsType>(TextBoundsType.class),
1448 DEFAULT_BOUNDS_TYPE) {
1449
1450 @Override
1451 public boolean isSettable(Text node) {
1452 return node.boundsType == null || !node.boundsType.isBound();
1453 }
1454
1455 @Override
1456 public StyleableProperty<TextBoundsType> getStyleableProperty(Text node) {
1457 return (StyleableProperty<TextBoundsType>)node.boundsTypeProperty();
1458 }
1459 };
1460
1461 private static final CssMetaData<Text,Number> TAB_SIZE =
1462 new CssMetaData<Text,Number>("-fx-tab-size",
1463 SizeConverter.getInstance(), TextLayout.DEFAULT_TAB_SIZE) {
1464
1465 @Override
1466 public boolean isSettable(Text node) {
1467 return node.attributes == null ||
1468 node.attributes.tabSize == null ||
1469 !node.attributes.tabSize.isBound();
1470 }
1471
1472 @Override
1473 public StyleableProperty<Number> getStyleableProperty(Text node) {
1474 return (StyleableProperty<Number>)node.tabSizeProperty();
1475 }
1476 };
1477
1478 private final static List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1479 static {
1480 final List<CssMetaData<? extends Styleable, ?>> styleables =
1481 new ArrayList<CssMetaData<? extends Styleable, ?>>(Shape.getClassCssMetaData());
1482 styleables.add(FONT);
1483 styleables.add(UNDERLINE);
1484 styleables.add(STRIKETHROUGH);
1485 styleables.add(TEXT_ALIGNMENT);
1486 styleables.add(TEXT_ORIGIN);
1487 styleables.add(FONT_SMOOTHING_TYPE);
1488 styleables.add(LINE_SPACING);
1489 styleables.add(BOUNDS_TYPE);
1490 styleables.add(TAB_SIZE);
1491 STYLEABLES = Collections.unmodifiableList(styleables);
1492 }
1493 }
1494
1495 /**
1496 * @return The CssMetaData associated with this class, which may include the
1497 * CssMetaData of its superclasses.
1498 * @since JavaFX 8.0
1499 */
1500 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1501 return StyleableProperties.STYLEABLES;
1502 }
1503
1504 /**
1505 * {@inheritDoc}
1506 *
1507 * @since JavaFX 8.0
1508 */
1509
1510
1511 @Override
1512 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1513 return getClassCssMetaData();
1514 }
1515
1516 @SuppressWarnings("deprecation")
1517 private void updatePGText() {
1518 final NGText peer = NodeHelper.getPeer(this);
1519 if (NodeHelper.isDirty(this, DirtyBits.TEXT_ATTRS)) {
1520 peer.setUnderline(isUnderline());
1521 peer.setStrikethrough(isStrikethrough());
1522 FontSmoothingType smoothing = getFontSmoothingType();
1523 if (smoothing == null) smoothing = FontSmoothingType.GRAY;
1524 peer.setFontSmoothingType(smoothing.ordinal());
1525 }
1526 if (NodeHelper.isDirty(this, DirtyBits.TEXT_FONT)) {
1527 peer.setFont(getFontInternal());
1528 }
1529 if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
1530 peer.setGlyphs(getRuns());
1531 }
1532 if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) {
1533 if (isSpan()) {
1534 BaseBounds spanBounds = getSpanBounds();
1535 peer.setLayoutLocation(spanBounds.getMinX(), spanBounds.getMinY());
1536 } else {
1537 float x = (float)getX();
1538 float y = (float)getY();
1539 float yadj = getYRendering();
1540 peer.setLayoutLocation(-x, yadj - y);
1541 }
1542 }
1543 if (NodeHelper.isDirty(this, DirtyBits.TEXT_SELECTION)) {
1544 Object fillObj = null;
1545 int start = getSelectionStart();
1546 int end = getSelectionEnd();
1547 int length = getTextInternal().length();
1548 if (0 <= start && start < end && end <= length) {
1549 Paint fill = selectionFillProperty().get();
1550 fillObj = fill != null ? Toolkit.getPaintAccessor().getPlatformPaint(fill) : null;
1551 }
1552 peer.setSelection(start, end, fillObj);
1553 }
1554 }
1555
1556 /*
1557 * Note: This method MUST only be called via its accessor method.
1558 */
1559 private void doUpdatePeer() {
1560 updatePGText();
1561 }
1562
1563 /***************************************************************************
1564 * *
1565 * Seldom Used Properties *
1566 * *
1567 **************************************************************************/
1568
1569 private TextAttribute attributes;
1570
1571 private TextAttribute getTextAttribute() {
1572 if (attributes == null) {
1573 attributes = new TextAttribute();
1574 }
1575 return attributes;
1576 }
1577
1578 private static final VPos DEFAULT_TEXT_ORIGIN = VPos.BASELINE;
1579 private static final TextBoundsType DEFAULT_BOUNDS_TYPE = TextBoundsType.LOGICAL;
1580 private static final boolean DEFAULT_UNDERLINE = false;
1581 private static final boolean DEFAULT_STRIKETHROUGH = false;
1582 private static final TextAlignment DEFAULT_TEXT_ALIGNMENT = TextAlignment.LEFT;
1583 private static final double DEFAULT_LINE_SPACING = 0;
1584 private static final int DEFAULT_CARET_POSITION = -1;
1585 private static final int DEFAULT_SELECTION_START = -1;
1586 private static final int DEFAULT_SELECTION_END = -1;
1587 private static final Color DEFAULT_SELECTION_FILL= Color.WHITE;
1588 private static final boolean DEFAULT_CARET_BIAS = true;
1589
1590 private final class TextAttribute {
1591
1592 private ObjectProperty<VPos> textOrigin;
1593
1594 final VPos getTextOrigin() {
1595 return textOrigin == null ? DEFAULT_TEXT_ORIGIN : textOrigin.get();
1596 }
1597
1598 public final ObjectProperty<VPos> textOriginProperty() {
1599 if (textOrigin == null) {
1600 textOrigin = new StyleableObjectProperty<VPos>(DEFAULT_TEXT_ORIGIN) {
1601 @Override public Object getBean() { return Text.this; }
1602 @Override public String getName() { return "textOrigin"; }
1603 @Override public CssMetaData getCssMetaData() {
1604 return StyleableProperties.TEXT_ORIGIN;
1605 }
1606 @Override public void invalidated() {
1607 NodeHelper.geomChanged(Text.this);
1608 }
1609 };
1610 }
1611 return textOrigin;
1612 }
1613
1614 private BooleanProperty underline;
1615
1616 final boolean isUnderline() {
1617 return underline == null ? DEFAULT_UNDERLINE : underline.get();
1618 }
1619
1620 final BooleanProperty underlineProperty() {
1621 if (underline == null) {
1622 underline = new StyleableBooleanProperty() {
1623 @Override public Object getBean() { return Text.this; }
1624 @Override public String getName() { return "underline"; }
1625 @Override public CssMetaData getCssMetaData() {
1626 return StyleableProperties.UNDERLINE;
1627 }
1628 @Override public void invalidated() {
1629 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS);
1630 if (getBoundsType() == TextBoundsType.VISUAL) {
1631 NodeHelper.geomChanged(Text.this);
1632 }
1633 }
1634 };
1635 }
1636 return underline;
1637 }
1638
1639 private BooleanProperty strikethrough;
1640
1641 final boolean isStrikethrough() {
1642 return strikethrough == null ? DEFAULT_STRIKETHROUGH : strikethrough.get();
1643 }
1644
1645 final BooleanProperty strikethroughProperty() {
1646 if (strikethrough == null) {
1647 strikethrough = new StyleableBooleanProperty() {
1648 @Override public Object getBean() { return Text.this; }
1649 @Override public String getName() { return "strikethrough"; }
1650 @Override public CssMetaData getCssMetaData() {
1651 return StyleableProperties.STRIKETHROUGH;
1652 }
1653 @Override public void invalidated() {
1654 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS);
1655 if (getBoundsType() == TextBoundsType.VISUAL) {
1656 NodeHelper.geomChanged(Text.this);
1657 }
1658 }
1659 };
1660 }
1661 return strikethrough;
1662 }
1663
1664 private ObjectProperty<TextAlignment> textAlignment;
1665
1666 final TextAlignment getTextAlignment() {
1667 return textAlignment == null ? DEFAULT_TEXT_ALIGNMENT : textAlignment.get();
1668 }
1669
1670 final ObjectProperty<TextAlignment> textAlignmentProperty() {
1671 if (textAlignment == null) {
1672 textAlignment =
1673 new StyleableObjectProperty<TextAlignment>(DEFAULT_TEXT_ALIGNMENT) {
1674 @Override public Object getBean() { return Text.this; }
1675 @Override public String getName() { return "textAlignment"; }
1676 @Override public CssMetaData getCssMetaData() {
1677 return StyleableProperties.TEXT_ALIGNMENT;
1678 }
1679 @Override public void invalidated() {
1680 if (!isSpan()) {
1681 TextAlignment alignment = get();
1682 if (alignment == null) {
1683 alignment = DEFAULT_TEXT_ALIGNMENT;
1684 }
1685 TextLayout layout = getTextLayout();
1686 if (layout.setAlignment(alignment.ordinal())) {
1687 needsTextLayout();
1688 }
1689 }
1690 }
1691 };
1692 }
1693 return textAlignment;
1694 }
1695
1696 private DoubleProperty lineSpacing;
1697
1698 final double getLineSpacing() {
1699 return lineSpacing == null ? DEFAULT_LINE_SPACING : lineSpacing.get();
1700 }
1701
1702 final DoubleProperty lineSpacingProperty() {
1703 if (lineSpacing == null) {
1704 lineSpacing =
1705 new StyleableDoubleProperty(DEFAULT_LINE_SPACING) {
1706 @Override public Object getBean() { return Text.this; }
1707 @Override public String getName() { return "lineSpacing"; }
1708 @Override public CssMetaData getCssMetaData() {
1709 return StyleableProperties.LINE_SPACING;
1710 }
1711 @Override public void invalidated() {
1712 if (!isSpan()) {
1713 TextLayout layout = getTextLayout();
1714 if (layout.setLineSpacing((float)get())) {
1715 needsTextLayout();
1716 }
1717 }
1718 }
1719 };
1720 }
1721 return lineSpacing;
1722 }
1723
1724 private ReadOnlyDoubleWrapper baselineOffset;
1725
1726 final ReadOnlyDoubleProperty baselineOffsetProperty() {
1727 if (baselineOffset == null) {
1728 baselineOffset = new ReadOnlyDoubleWrapper(Text.this, "baselineOffset") {
1729 {bind(new DoubleBinding() {
1730 {bind(fontProperty());}
1731 @Override protected double computeValue() {
1732 /* This method should never be used for spans.
1733 * If it is, it will still returns the ascent
1734 * for the first line in the layout */
1735 BaseBounds bounds = getLogicalBounds();
1736 return -bounds.getMinY();
1737 }
1738 });}
1739 };
1740 }
1741 return baselineOffset.getReadOnlyProperty();
1742 }
1743
1744 private ObjectProperty<PathElement[]> selectionShape;
1745 private ObjectBinding<PathElement[]> selectionBinding;
1746
1747 final ReadOnlyObjectProperty<PathElement[]> selectionShapeProperty() {
1748 if (selectionShape == null) {
1749 selectionBinding = new ObjectBinding<PathElement[]>() {
1750 {bind(selectionStartProperty(), selectionEndProperty());}
1751 @Override protected PathElement[] computeValue() {
1752 int start = getSelectionStart();
1753 int end = getSelectionEnd();
1754 return getRange(start, end, TextLayout.TYPE_TEXT);
1755 }
1756 };
1757 selectionShape = new SimpleObjectProperty<PathElement[]>(Text.this, "selectionShape");
1758 selectionShape.bind(selectionBinding);
1759 }
1760 return selectionShape;
1761 }
1762
1763 private ObjectProperty<Paint> selectionFill;
1764
1765 final ObjectProperty<Paint> selectionFillProperty() {
1766 if (selectionFill == null) {
1767 selectionFill =
1768 new ObjectPropertyBase<Paint>(DEFAULT_SELECTION_FILL) {
1769 @Override public Object getBean() { return Text.this; }
1770 @Override public String getName() { return "selectionFill"; }
1771 @Override protected void invalidated() {
1772 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION);
1773 }
1774 };
1775 }
1776 return selectionFill;
1777 }
1778
1779 private IntegerProperty selectionStart;
1780
1781 final int getSelectionStart() {
1782 return selectionStart == null ? DEFAULT_SELECTION_START : selectionStart.get();
1783 }
1784
1785 final IntegerProperty selectionStartProperty() {
1786 if (selectionStart == null) {
1787 selectionStart =
1788 new IntegerPropertyBase(DEFAULT_SELECTION_START) {
1789 @Override public Object getBean() { return Text.this; }
1790 @Override public String getName() { return "selectionStart"; }
1791 @Override protected void invalidated() {
1792 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION);
1793 notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_START);
1794 }
1795 };
1796 }
1797 return selectionStart;
1798 }
1799
1800 private IntegerProperty selectionEnd;
1801
1802 final int getSelectionEnd() {
1803 return selectionEnd == null ? DEFAULT_SELECTION_END : selectionEnd.get();
1804 }
1805
1806 final IntegerProperty selectionEndProperty() {
1807 if (selectionEnd == null) {
1808 selectionEnd =
1809 new IntegerPropertyBase(DEFAULT_SELECTION_END) {
1810 @Override public Object getBean() { return Text.this; }
1811 @Override public String getName() { return "selectionEnd"; }
1812 @Override protected void invalidated() {
1813 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_SELECTION);
1814 notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END);
1815 }
1816 };
1817 }
1818 return selectionEnd;
1819 }
1820
1821 private ObjectProperty<PathElement[]> caretShape;
1822 private ObjectBinding<PathElement[]> caretBinding;
1823
1824 final ReadOnlyObjectProperty<PathElement[]> caretShapeProperty() {
1825 if (caretShape == null) {
1826 caretBinding = new ObjectBinding<PathElement[]>() {
1827 {bind(caretPositionProperty(), caretBiasProperty());}
1828 @Override protected PathElement[] computeValue() {
1829 int pos = getCaretPosition();
1830 int length = getTextInternal().length();
1831 if (0 <= pos && pos <= length) {
1832 boolean bias = isCaretBias();
1833 float x = (float)getX();
1834 float y = (float)getY() - getYRendering();
1835 TextLayout layout = getTextLayout();
1836 return layout.getCaretShape(pos, bias, x, y);
1837 }
1838 return EMPTY_PATH_ELEMENT_ARRAY;
1839 }
1840 };
1841 caretShape = new SimpleObjectProperty<PathElement[]>(Text.this, "caretShape");
1842 caretShape.bind(caretBinding);
1843 }
1844 return caretShape;
1845 }
1846
1847 private IntegerProperty caretPosition;
1848
1849 final int getCaretPosition() {
1850 return caretPosition == null ? DEFAULT_CARET_POSITION : caretPosition.get();
1851 }
1852
1853 final IntegerProperty caretPositionProperty() {
1854 if (caretPosition == null) {
1855 caretPosition =
1856 new IntegerPropertyBase(DEFAULT_CARET_POSITION) {
1857 @Override public Object getBean() { return Text.this; }
1858 @Override public String getName() { return "caretPosition"; }
1859 @Override protected void invalidated() {
1860 notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_END);
1861 }
1862 };
1863 }
1864 return caretPosition;
1865 }
1866
1867 private BooleanProperty caretBias;
1868
1869 final boolean isCaretBias() {
1870 return caretBias == null ? DEFAULT_CARET_BIAS : caretBias.get();
1871 }
1872
1873 final BooleanProperty caretBiasProperty() {
1874 if (caretBias == null) {
1875 caretBias =
1876 new SimpleBooleanProperty(Text.this, "caretBias", DEFAULT_CARET_BIAS);
1877 }
1878 return caretBias;
1879 }
1880
1881 private IntegerProperty tabSize;
1882
1883 final int getTabSize() {
1884 return tabSize == null ? TextLayout.DEFAULT_TAB_SIZE : tabSize.get();
1885 }
1886
1887 final IntegerProperty tabSizeProperty() {
1888 if (tabSize == null) {
1889 tabSize = new StyleableIntegerProperty(TextLayout.DEFAULT_TAB_SIZE) {
1890 @Override public Object getBean() { return Text.this; }
1891 @Override public String getName() { return "tabSize"; }
1892 @Override public CssMetaData getCssMetaData() {
1893 return StyleableProperties.TAB_SIZE;
1894 }
1895 @Override public void set(int v) { super.set((v < 1) ? 1 : v); }
1896 @Override protected void invalidated() {
1897 TextLayout layout = getTextLayout();
1898 if (layout.setTabSize(get())) {
1899 needsTextLayout();
1900 }
1901 NodeHelper.markDirty(Text.this, DirtyBits.TEXT_ATTRS);
1902 if (getBoundsType() == TextBoundsType.VISUAL) {
1903 NodeHelper.geomChanged(Text.this);
1904 }
1905 }
1906 };
1907 }
1908 return tabSize;
1909 }
1910 }
1911
1912 /**
1913 * Returns a string representation of this {@code Text} object.
1914 * @return a string representation of this {@code Text} object.
1915 */
1916 @Override
1917 public String toString() {
1918 final StringBuilder sb = new StringBuilder("Text[");
1919
1920 String id = getId();
1921 if (id != null) {
1922 sb.append("id=").append(id).append(", ");
1923 }
1924
1925 sb.append("text=\"").append(getText()).append("\"");
1926 sb.append(", x=").append(getX());
1927 sb.append(", y=").append(getY());
1928 sb.append(", alignment=").append(getTextAlignment());
1929 sb.append(", origin=").append(getTextOrigin());
1930 sb.append(", boundsType=").append(getBoundsType());
1931
1932 double spacing = getLineSpacing();
1933 if (spacing != DEFAULT_LINE_SPACING) {
1934 sb.append(", lineSpacing=").append(spacing);
1935 }
1936
1937 double wrap = getWrappingWidth();
1938 if (wrap != 0) {
1939 sb.append(", wrappingWidth=").append(wrap);
1940 }
1941
1942 sb.append(", font=").append(getFont());
1943 sb.append(", fontSmoothingType=").append(getFontSmoothingType());
1944
1945 if (isStrikethrough()) {
1946 sb.append(", strikethrough");
1947 }
1948 if (isUnderline()) {
1949 sb.append(", underline");
1950 }
1951
1952 sb.append(", fill=").append(getFill());
1953
1954 Paint stroke = getStroke();
1955 if (stroke != null) {
1956 sb.append(", stroke=").append(stroke);
1957 sb.append(", strokeWidth=").append(getStrokeWidth());
1958 }
1959
1960 return sb.append("]").toString();
1961 }
1962
1963 /** {@inheritDoc} */
1964 @Override
1965 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1966 switch (attribute) {
1967 case TEXT: {
1968 String accText = getAccessibleText();
1969 if (accText != null && !accText.isEmpty()) return accText;
1970 return getText();
1971 }
1972 case FONT: return getFont();
1973 case CARET_OFFSET: {
1974 int sel = getCaretPosition();
1975 if (sel >= 0) return sel;
1976 return getText().length();
1977 }
1978 case SELECTION_START: {
1979 int sel = getSelectionStart();
1980 if (sel >= 0) return sel;
1981 sel = getCaretPosition();
1982 if (sel >= 0) return sel;
1983 return getText().length();
1984 }
1985 case SELECTION_END: {
1986 int sel = getSelectionEnd();
1987 if (sel >= 0) return sel;
1988 sel = getCaretPosition();
1989 if (sel >= 0) return sel;
1990 return getText().length();
1991 }
1992 case LINE_FOR_OFFSET: {
1993 int offset = (Integer)parameters[0];
1994 if (offset > getTextInternal().length()) return null;
1995 TextLine[] lines = getTextLayout().getLines();
1996 int lineIndex = 0;
1997 for (int i = 1; i < lines.length; i++) {
1998 TextLine line = lines[i];
1999 if (line.getStart() > offset) break;
2000 lineIndex++;
2001 }
2002 return lineIndex;
2003 }
2004 case LINE_START: {
2005 int lineIndex = (Integer)parameters[0];
2006 TextLine[] lines = getTextLayout().getLines();
2007 if (0 <= lineIndex && lineIndex < lines.length) {
2008 TextLine line = lines[lineIndex];
2009 return line.getStart();
2010 }
2011 return null;
2012 }
2013 case LINE_END: {
2014 int lineIndex = (Integer)parameters[0];
2015 TextLine[] lines = getTextLayout().getLines();
2016 if (0 <= lineIndex && lineIndex < lines.length) {
2017 TextLine line = lines[lineIndex];
2018 return line.getStart() + line.getLength();
2019 }
2020 return null;
2021 }
2022 case OFFSET_AT_POINT: {
2023 Point2D point = (Point2D)parameters[0];
2024 point = screenToLocal(point);
2025 return hitTest(point).getCharIndex();
2026 }
2027 case BOUNDS_FOR_RANGE: {
2028 int start = (Integer)parameters[0];
2029 int end = (Integer)parameters[1];
2030 PathElement[] elements = rangeShape(start, end + 1);
2031 /* Each bounds is defined by a MoveTo (top-left) followed by
2032 * 4 LineTo (to top-right, bottom-right, bottom-left, back to top-left).
2033 */
2034 Bounds[] bounds = new Bounds[elements.length / 5];
2035 int index = 0;
2036 for (int i = 0; i < bounds.length; i++) {
2037 MoveTo topLeft = (MoveTo)elements[index];
2038 LineTo topRight = (LineTo)elements[index+1];
2039 LineTo bottomRight = (LineTo)elements[index+2];
2040 BoundingBox b = new BoundingBox(topLeft.getX(), topLeft.getY(),
2041 topRight.getX() - topLeft.getX(),
2042 bottomRight.getY() - topRight.getY());
2043 bounds[i] = localToScreen(b);
2044 index += 5;
2045 }
2046 return bounds;
2047 }
2048 default: return super.queryAccessibleAttribute(attribute, parameters);
2049 }
2050 }
2051 }