1 /*
  2  * Copyright (c) 2012, 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 java.util.ArrayList;
 29 import java.util.Collections;
 30 import java.util.List;
 31 import javafx.beans.property.DoubleProperty;
 32 import javafx.beans.property.ObjectProperty;
 33 import javafx.geometry.HPos;
 34 import javafx.geometry.Insets;
 35 import javafx.geometry.NodeOrientation;
 36 import javafx.geometry.Orientation;
 37 import javafx.geometry.VPos;
 38 import javafx.scene.AccessibleAttribute;
 39 import javafx.scene.AccessibleRole;
 40 import javafx.scene.Node;
 41 import javafx.scene.layout.Pane;
 42 import javafx.scene.shape.PathElement;
 43 import javafx.css.StyleableDoubleProperty;
 44 import javafx.css.StyleableObjectProperty;
 45 import javafx.css.CssMetaData;
 46 import javafx.css.converter.EnumConverter;
 47 import javafx.css.converter.SizeConverter;
 48 import com.sun.javafx.geom.BaseBounds;
 49 import com.sun.javafx.geom.Point2D;
 50 import com.sun.javafx.geom.RectBounds;
 51 import com.sun.javafx.scene.text.GlyphList;
 52 import com.sun.javafx.scene.text.TextLayout;
 53 import com.sun.javafx.scene.text.TextLayoutFactory;
 54 import com.sun.javafx.scene.text.TextSpan;
 55 import com.sun.javafx.tk.Toolkit;
 56 import javafx.beans.property.IntegerProperty;
 57 import javafx.beans.property.IntegerPropertyBase;
 58 import javafx.css.Styleable;
 59 import javafx.css.StyleableIntegerProperty;
 60 import javafx.css.StyleableProperty;
 61 
 62 /**
 63  * TextFlow is special layout designed to lay out rich text.
 64  * It can be used to layout several {@link Text} nodes in a single text flow.
 65  * The TextFlow uses the text and the font of each {@link Text} node inside of it
 66  * plus its own width and text alignment to determine the location for each child.
 67  * A single {@link Text} node can span over several lines due to wrapping, and
 68  * the visual location of {@link Text} node can differ from the logical location
 69  * due to bidi reordering.
 70  *
 71  * <p>
 72  * Any Node, other than Text, will be treated as an embedded object in the
 73  * text layout. It will be inserted in the content using its preferred width,
 74  * height, and baseline offset.
 75  *
 76  * <p>
 77  * When a {@link Text} node is inside of a TextFlow, some of its properties are ignored.
 78  * For example, the x and y properties of the {@link Text} node are ignored since
 79  * the location of the node is determined by the parent. Likewise, the wrapping
 80  * width in the {@link Text} node is ignored since the width used for wrapping
 81  * is the TextFlow's width. The value of the <code>pickOnBounds</code> property
 82  * of a {@link Text} is set to <code>false</code> when it is laid out by the
 83  * TextFlow. This happens because the content of a single {@link Text} node can
 84  * divided and placed in the different locations on the TextFlow (usually due to
 85  * line breaking and bidi reordering).
 86  *
 87  * <p>
 88  * The wrapping width of the layout is determined by the region's current width.
 89  * It can be specified by the application by setting the textflow's preferred
 90  * width. If no wrapping is desired, the application can either set the preferred
 91  * with to Double.MAX_VALUE or Region.USE_COMPUTED_SIZE.
 92  *
 93  * <p>
 94  * Paragraphs are separated by {@code '\n'} present in any Text child.
 95  *
 96  * <p>
 97  * Example of a TextFlow:
 98  * <pre>{@code
 99  *     Text text1 = new Text("Big italic red text");
100  *     text1.setFill(Color.RED);
101  *     text1.setFont(Font.font("Helvetica", FontPosture.ITALIC, 40));
102  *     Text text2 = new Text(" little bold blue text");
103  *     text2.setFill(Color.BLUE);
104  *     text2.setFont(Font.font("Helvetica", FontWeight.BOLD, 10));
105  *     TextFlow textFlow = new TextFlow(text1, text2);
106  * }</pre>
107  *
108  * <p>
109  * TextFlow lays out each managed child regardless of the child's visible property value;
110  * unmanaged children are ignored for all layout calculations.</p>
111  *
112  * <p>
113  * TextFlow may be styled with backgrounds and borders using CSS.  See
114  * {@link javafx.scene.layout.Region Region} superclass for details.</p>
115  *
116  * <h2>Resizable Range</h2>
117  *
118  * <p>
119  * A textflow's parent will resize the textflow within the textflow's range
120  * during layout. By default the textflow computes this range based on its content
121  * as outlined in the tables below.
122  * </p>
123  *
124  * <table border="1">
125  * <caption>TextFlow Resize Table</caption>
126  * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr>
127  * <tr><th scope="row">minimum</th>
128  * <td>left/right insets</td>
129  * <td>top/bottom insets plus the height of the text content</td></tr>
130  * <tr><th scope="row">preferred</th>
131  * <td>left/right insets plus the width of the text content</td>
132  * <td>top/bottom insets plus the height of the text content</td></tr>
133  * <tr><th scope="row">maximum</th>
134  * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
135  * </table>
136  * <p>
137  * A textflow's unbounded maximum width and height are an indication to the parent that
138  * it may be resized beyond its preferred size to fill whatever space is assigned to it.
139  * <p>
140  * TextFlow provides properties for setting the size range directly.  These
141  * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the
142  * application may set them to other values as needed:
143  * <pre><code>
144  *     <b>textflow.setMaxWidth(500);</b>
145  * </code></pre>
146  * Applications may restore the computed values by setting these properties back
147  * to Region.USE_COMPUTED_SIZE.
148  * <p>
149  * TextFlow does not clip its content by default, so it is possible that childrens'
150  * bounds may extend outside its own bounds if a child's pref size is larger than
151  * the space textflow has to allocate for it.</p>
152  *
153  * @since JavaFX 8.0
154  */
155 public class TextFlow extends Pane {
156 
157     private TextLayout layout;
158     private boolean needsContent;
159     private boolean inLayout;
160 
161     /**
162      * Creates an empty TextFlow layout.
163      */
164     public TextFlow() {
165         super();
166         effectiveNodeOrientationProperty().addListener(observable -> checkOrientation());
167         setAccessibleRole(AccessibleRole.TEXT);
168     }
169 
170     /**
171      * Creates a TextFlow layout with the given children.
172      *
173      * @param children children.
174      */
175     public TextFlow(Node... children) {
176         this();
177         getChildren().addAll(children);
178     }
179 
180     private void checkOrientation() {
181         NodeOrientation orientation = getEffectiveNodeOrientation();
182         boolean rtl =  orientation == NodeOrientation.RIGHT_TO_LEFT;
183         int dir = rtl ? TextLayout.DIRECTION_RTL : TextLayout.DIRECTION_LTR;
184         TextLayout layout = getTextLayout();
185         if (layout.setDirection(dir)) {
186             requestLayout();
187         }
188     }
189 
190     /**
191      * Maps local point to index in the content.
192      *
193      * @param point the specified point to be tested
194      * @return a {@code HitInfo} representing the character index found
195      * @since 9
196      */
197     public final HitInfo hitTest(javafx.geometry.Point2D point) {
198         if (point != null) {
199             TextLayout layout = getTextLayout();
200             double x = point.getX()/* - getX()*/;
201             double y = point.getY()/* - getY()/* + getYRendering()*/;
202             TextLayout.Hit layoutHit = layout.getHitInfo((float)x, (float)y);
203             return new HitInfo(layoutHit.getCharIndex(), layoutHit.getInsertionIndex(),
204                                layoutHit.isLeading(), null/*getText()*/);
205         } else {
206             return null;
207         }
208     }
209 
210     /**
211      * Returns shape of caret in local coordinates.
212      *
213      * @param charIndex the character index for the caret
214      * @param leading whether the caret is biased on the leading edge of the character
215      * @return an array of {@code PathElement} which can be used to create a {@code Shape}
216      * @since 9
217      */
218     public PathElement[] caretShape(int charIndex, boolean leading) {
219         return getTextLayout().getCaretShape(charIndex, leading, 0, 0);
220     }
221 
222     /**
223      * Returns shape for the range of the text in local coordinates.
224      *
225      * @param start the beginning character index for the range
226      * @param end the end character index (non-inclusive) for the range
227      * @return an array of {@code PathElement} which can be used to create a {@code Shape}
228      * @since 9
229      */
230     public final PathElement[] rangeShape(int start, int end) {
231         return getRange(start, end, TextLayout.TYPE_TEXT);
232     }
233 
234     @Override
235     public boolean usesMirroring() {
236         return false;
237     }
238 
239     @Override protected void setWidth(double value) {
240         if (value != getWidth()) {
241             TextLayout layout = getTextLayout();
242             Insets insets = getInsets();
243             double left = snapSpaceX(insets.getLeft());
244             double right = snapSpaceX(insets.getRight());
245             double width = Math.max(1, value - left - right);
246             layout.setWrapWidth((float)width);
247             super.setWidth(value);
248         }
249     }
250 
251     @Override protected double computePrefWidth(double height) {
252         TextLayout layout = getTextLayout();
253         layout.setWrapWidth(0);
254         double width = layout.getBounds().getWidth();
255         Insets insets = getInsets();
256         double left = snapSpaceX(insets.getLeft());
257         double right = snapSpaceX(insets.getRight());
258         double wrappingWidth = Math.max(1, getWidth() - left - right);
259         layout.setWrapWidth((float)wrappingWidth);
260         return left + width + right;
261     }
262 
263     @Override protected double computePrefHeight(double width) {
264         TextLayout layout = getTextLayout();
265         Insets insets = getInsets();
266         double left = snapSpaceX(insets.getLeft());
267         double right = snapSpaceX(insets.getRight());
268         if (width == USE_COMPUTED_SIZE) {
269             layout.setWrapWidth(0);
270         } else {
271             double wrappingWidth = Math.max(1, width - left - right);
272             layout.setWrapWidth((float)wrappingWidth);
273         }
274         double height = layout.getBounds().getHeight();
275         double wrappingWidth = Math.max(1, getWidth() - left - right);
276         layout.setWrapWidth((float)wrappingWidth);
277         double top = snapSpaceY(insets.getTop());
278         double bottom = snapSpaceY(insets.getBottom());
279         return top + height + bottom;
280     }
281 
282     @Override protected double computeMinHeight(double width) {
283         return computePrefHeight(width);
284     }
285 
286     @Override public void requestLayout() {
287         /* The geometry of text nodes can be changed during layout children.
288          * For that reason it has to call NodeHelper.geomChanged(this) causing
289          * requestLayout() to happen during layoutChildren().
290          * The inLayout flag prevents this call to cause any extra work.
291          */
292         if (inLayout) return;
293 
294         /*
295         * There is no need to reset the text layout's content every time
296         * requestLayout() is called. For example, the content needs
297         * to be set when:
298         *  children add or removed
299         *  children managed state changes
300         *  children geomChanged (width/height of embedded node)
301         *  children content changes (text/font of text node)
302         * The content does not need to set when:
303         *  the width/height changes in the region
304         *  the insets changes in the region
305         *
306         * Unfortunately, it is not possible to know what change invoked request
307         * layout. The solution is to always reset the content in the text
308         * layout and rely on it to preserve itself if the new content equals to
309         * the old one. The cost to generate the new content is not avoid.
310         */
311         needsContent = true;
312         super.requestLayout();
313     }
314 
315     @Override public Orientation getContentBias() {
316         return Orientation.HORIZONTAL;
317     }
318 
319     @Override protected void layoutChildren() {
320         inLayout = true;
321         Insets insets = getInsets();
322         double top = snapSpaceY(insets.getTop());
323         double left = snapSpaceX(insets.getLeft());
324 
325         GlyphList[] runs = getTextLayout().getRuns();
326         for (int j = 0; j < runs.length; j++) {
327             GlyphList run = runs[j];
328             TextSpan span = run.getTextSpan();
329             if (span instanceof EmbeddedSpan) {
330                 Node child = ((EmbeddedSpan)span).getNode();
331                 Point2D location = run.getLocation();
332                 double baselineOffset = -run.getLineBounds().getMinY();
333 
334                 layoutInArea(child, left + location.x, top + location.y,
335                              run.getWidth(), run.getHeight(),
336                              baselineOffset, null, true, true,
337                              HPos.CENTER, VPos.BASELINE);
338             }
339         }
340 
341         List<Node> managed = getManagedChildren();
342         for (Node node: managed) {
343             if (node instanceof Text) {
344                 Text text = (Text)node;
345                 text.layoutSpan(runs);
346                 BaseBounds spanBounds = text.getSpanBounds();
347                 text.relocate(left + spanBounds.getMinX(),
348                               top + spanBounds.getMinY());
349             }
350         }
351         inLayout = false;
352     }
353 
354     private PathElement[] getRange(int start, int end, int type) {
355         TextLayout layout = getTextLayout();
356         return layout.getRange(start, end, type, 0, 0);
357     }
358 
359     private static class EmbeddedSpan implements TextSpan {
360         RectBounds bounds;
361         Node node;
362         public EmbeddedSpan(Node node, double baseline, double width, double height) {
363             this.node = node;
364             bounds = new RectBounds(0, (float)-baseline,
365                                     (float)width, (float)(height - baseline));
366         }
367 
368         @Override public String getText() {
369             return "\uFFFC";
370         }
371 
372         @Override public Object getFont() {
373             return null;
374         }
375 
376         @Override public RectBounds getBounds() {
377             return bounds;
378         }
379 
380         public Node getNode() {
381             return node;
382         }
383     }
384 
385     TextLayout getTextLayout() {
386         if (layout == null) {
387             TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory();
388             layout = factory.createLayout();
389             layout.setTabSize(getTabSize());
390             needsContent = true;
391         }
392         if (needsContent) {
393             List<Node> children = getManagedChildren();
394             TextSpan[] spans = new TextSpan[children.size()];
395             for (int i = 0; i < spans.length; i++) {
396                 Node node = children.get(i);
397                 if (node instanceof Text) {
398                     spans[i] = ((Text)node).getTextSpan();
399                 } else {
400                     /* Creating a text span every time forces text layout
401                      * to run a full text analysis in the new content.
402                      */
403                     double baseline = node.getBaselineOffset();
404                     if (baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) {
405                         baseline = node.getLayoutBounds().getHeight();
406                     }
407                     double width = computeChildPrefAreaWidth(node, null);
408                     double height = computeChildPrefAreaHeight(node, null);
409                     spans[i] = new EmbeddedSpan(node, baseline, width, height);
410                 }
411             }
412             layout.setContent(spans);
413             needsContent = false;
414         }
415         return layout;
416     }
417 
418     /**
419      * Defines horizontal text alignment.
420      *
421      * @defaultValue TextAlignment.LEFT
422      */
423     private ObjectProperty<TextAlignment> textAlignment;
424 
425     public final void setTextAlignment(TextAlignment value) {
426         textAlignmentProperty().set(value);
427     }
428 
429     public final TextAlignment getTextAlignment() {
430         return textAlignment == null ? TextAlignment.LEFT : textAlignment.get();
431     }
432 
433     public final ObjectProperty<TextAlignment> textAlignmentProperty() {
434         if (textAlignment == null) {
435             textAlignment =
436                 new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) {
437                 @Override public Object getBean() { return TextFlow.this; }
438                 @Override public String getName() { return "textAlignment"; }
439                 @Override public CssMetaData<TextFlow, TextAlignment> getCssMetaData() {
440                     return StyleableProperties.TEXT_ALIGNMENT;
441                 }
442                 @Override public void invalidated() {
443                     TextAlignment align = get();
444                     if (align == null) align = TextAlignment.LEFT;
445                     TextLayout layout = getTextLayout();
446                     layout.setAlignment(align.ordinal());
447                     requestLayout();
448                 }
449             };
450         }
451         return textAlignment;
452     }
453 
454     /**
455      * Defines the vertical space in pixel between lines.
456      *
457      * @defaultValue 0
458      *
459      * @since JavaFX 8.0
460      */
461     private DoubleProperty lineSpacing;
462 
463     public final void setLineSpacing(double spacing) {
464         lineSpacingProperty().set(spacing);
465     }
466 
467     public final double getLineSpacing() {
468         return lineSpacing == null ? 0 : lineSpacing.get();
469     }
470 
471     public final DoubleProperty lineSpacingProperty() {
472         if (lineSpacing == null) {
473             lineSpacing =
474                 new StyleableDoubleProperty(0) {
475                 @Override public Object getBean() { return TextFlow.this; }
476                 @Override public String getName() { return "lineSpacing"; }
477                 @Override public CssMetaData<TextFlow, Number> getCssMetaData() {
478                     return StyleableProperties.LINE_SPACING;
479                 }
480                 @Override public void invalidated() {
481                     TextLayout layout = getTextLayout();
482                     if (layout.setLineSpacing((float)get())) {
483                         requestLayout();
484                     }
485                 }
486             };
487         }
488         return lineSpacing;
489     }
490     /**
491      * The size of a tab stop in spaces.
492      *
493      * @return the {@code tabSize} property
494      *
495      * @defaultValue {@code 8}
496      *
497      * @since 14
498      */
499     private IntegerProperty tabSize;
500 
501     /**
502      * Gets the size of a tab stop in spaces.
503      * @return the size of a tab in spaces
504      * @since 14
505      */
506     public final int getTabSize() {
507         return tabSize == null ? TextLayout.DEFAULT_TAB_SIZE : tabSize.get();
508     }
509 
510     /**
511      * Sets the size of a tab stop.
512      * @param spaces the size of a tab in spaces. Defaults to 8.
513      * Minimum is 1, lower values will be clamped to 1.
514      * @since 14
515     */
516     public final void setTabSize(int spaces) {
517         tabSizeProperty().set(spaces);
518     }
519 
520     final IntegerProperty tabSizeProperty() {
521         if (tabSize == null) {
522             tabSize = new StyleableIntegerProperty(TextLayout.DEFAULT_TAB_SIZE) {
523                 @Override public Object getBean() { return TextFlow.this; }
524                 @Override public String getName() { return "tabSize"; }
525                 @Override public CssMetaData getCssMetaData() {
526                     return StyleableProperties.TAB_SIZE;
527                 }
528                 @Override public void set(int v) { super.set((v < 1) ? 1 : v); }
529                 @Override protected void invalidated() {
530                     TextLayout layout = getTextLayout();
531                     if (layout.setTabSize(get())) {
532                         requestLayout();
533                     }
534                 }
535             };
536         }
537         return tabSize;
538     }
539 
540     @Override public final double getBaselineOffset() {
541         Insets insets = getInsets();
542         double top = snapSpaceY(insets.getTop());
543         return top - getTextLayout().getBounds().getMinY();
544     }
545 
546    /***************************************************************************
547     *                                                                         *
548     *                            Stylesheet Handling                          *
549     *                                                                         *
550     **************************************************************************/
551 
552     /*
553      * Super-lazy instantiation pattern from Bill Pugh.
554      */
555     private static class StyleableProperties {
556 
557         private static final
558             CssMetaData<TextFlow, TextAlignment> TEXT_ALIGNMENT =
559                 new CssMetaData<TextFlow,TextAlignment>("-fx-text-alignment",
560                 new EnumConverter<TextAlignment>(TextAlignment.class),
561                 TextAlignment.LEFT) {
562 
563             @Override public boolean isSettable(TextFlow node) {
564                 return node.textAlignment == null || !node.textAlignment.isBound();
565             }
566 
567             @Override public StyleableProperty<TextAlignment> getStyleableProperty(TextFlow node) {
568                 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty();
569             }
570         };
571 
572         private static final
573             CssMetaData<TextFlow,Number> LINE_SPACING =
574                 new CssMetaData<TextFlow,Number>("-fx-line-spacing",
575                 SizeConverter.getInstance(), 0) {
576 
577             @Override public boolean isSettable(TextFlow node) {
578                 return node.lineSpacing == null || !node.lineSpacing.isBound();
579             }
580 
581             @Override public StyleableProperty<Number> getStyleableProperty(TextFlow node) {
582                 return (StyleableProperty<Number>)node.lineSpacingProperty();
583             }
584         };
585 
586         private static final CssMetaData<TextFlow,Number> TAB_SIZE =
587                 new CssMetaData<TextFlow,Number>("-fx-tab-size",
588                 SizeConverter.getInstance(), TextLayout.DEFAULT_TAB_SIZE) {
589 
590             @Override
591             public boolean isSettable(TextFlow node) {
592                 return node.tabSize == null || !node.tabSize.isBound();
593             }
594 
595             @Override
596             public StyleableProperty<Number> getStyleableProperty(TextFlow node) {
597                 return (StyleableProperty<Number>)node.tabSizeProperty();
598             }
599         };
600 
601         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
602         static {
603             final List<CssMetaData<? extends Styleable, ?>> styleables =
604                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Pane.getClassCssMetaData());
605             styleables.add(TEXT_ALIGNMENT);
606             styleables.add(LINE_SPACING);
607             styleables.add(TAB_SIZE);
608             STYLEABLES = Collections.unmodifiableList(styleables);
609         }
610     }
611 
612     /**
613      * @return The CssMetaData associated with this class, which may include the
614      * CssMetaData of its superclasses.
615      */
616     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
617         return StyleableProperties.STYLEABLES;
618     }
619 
620     @Override
621     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
622         return getClassCssMetaData();
623     }
624 
625     /* The methods in this section are copied from Region due to package visibility restriction */
626     private static double snapSpace(double value, boolean snapToPixel) {
627         return snapToPixel ? Math.round(value) : value;
628     }
629 
630     static double boundedSize(double min, double pref, double max) {
631         double a = pref >= min ? pref : min;
632         double b = min >= max ? min : max;
633         return a <= b ? a : b;
634     }
635 
636     double computeChildPrefAreaWidth(Node child, Insets margin) {
637         return computeChildPrefAreaWidth(child, margin, -1);
638     }
639 
640     double computeChildPrefAreaWidth(Node child, Insets margin, double height) {
641         final boolean snap = isSnapToPixel();
642         double top = margin != null? snapSpace(margin.getTop(), snap) : 0;
643         double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0;
644         double left = margin != null? snapSpace(margin.getLeft(), snap) : 0;
645         double right = margin != null? snapSpace(margin.getRight(), snap) : 0;
646         double alt = -1;
647         if (child.getContentBias() == Orientation.VERTICAL) { // width depends on height
648             alt = snapSizeY(boundedSize(
649                     child.minHeight(-1), height != -1? height - top - bottom :
650                            child.prefHeight(-1), child.maxHeight(-1)));
651         }
652         return left + snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right;
653     }
654 
655     double computeChildPrefAreaHeight(Node child, Insets margin) {
656         return computeChildPrefAreaHeight(child, margin, -1);
657     }
658 
659     double computeChildPrefAreaHeight(Node child, Insets margin, double width) {
660         final boolean snap = isSnapToPixel();
661         double top = margin != null? snapSpace(margin.getTop(), snap) : 0;
662         double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0;
663         double left = margin != null? snapSpace(margin.getLeft(), snap) : 0;
664         double right = margin != null? snapSpace(margin.getRight(), snap) : 0;
665         double alt = -1;
666         if (child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width
667             alt = snapSizeX(boundedSize(
668                     child.minWidth(-1), width != -1? width - left - right :
669                            child.prefWidth(-1), child.maxWidth(-1)));
670         }
671         return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom;
672     }
673     /* end of copied code */
674 
675     /** {@inheritDoc} */
676     @Override
677     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
678         switch (attribute) {
679             case TEXT: {
680                 String accText = getAccessibleText();
681                 if (accText != null && !accText.isEmpty()) return accText;
682 
683                 StringBuilder title = new StringBuilder();
684                 for (Node node: getChildren()) {
685                     Object text = node.queryAccessibleAttribute(AccessibleAttribute.TEXT, parameters);
686                     if (text != null) {
687                         title.append(text.toString());
688                     }
689                 }
690                 return title.toString();
691             }
692             default: return super.queryAccessibleAttribute(attribute, parameters);
693         }
694     }
695 }