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 }