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 }