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