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