1 /*
   2  * Copyright (c) 2011, 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.control;
  27 
  28 import java.util.AbstractList;
  29 import java.util.ArrayList;
  30 import java.util.Collection;
  31 import java.util.Collections;
  32 import java.util.List;
  33 
  34 import javafx.beans.InvalidationListener;
  35 import javafx.beans.property.BooleanProperty;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.beans.property.IntegerProperty;
  38 import javafx.beans.property.SimpleDoubleProperty;
  39 import javafx.beans.value.ChangeListener;
  40 import javafx.beans.value.WritableValue;
  41 import javafx.collections.ListChangeListener;
  42 import javafx.collections.ObservableList;
  43 import javafx.css.CssMetaData;
  44 import javafx.css.StyleConverter;
  45 import javafx.css.StyleableBooleanProperty;
  46 import javafx.css.StyleableIntegerProperty;
  47 import javafx.css.StyleableProperty;
  48 
  49 import com.sun.javafx.binding.ExpressionHelper;
  50 import com.sun.javafx.collections.ListListenerHelper;
  51 import com.sun.javafx.collections.NonIterableChange;
  52 import javafx.css.converter.SizeConverter;
  53 import javafx.scene.control.skin.TextAreaSkin;
  54 
  55 import javafx.css.Styleable;
  56 import javafx.scene.AccessibleRole;
  57 
  58 /**
  59  * Text input component that allows a user to enter multiple lines of
  60  * plain text. Unlike in previous releases of JavaFX, support for single line
  61  * input is not available as part of the TextArea control, however this is
  62  * the sole-purpose of the {@link TextField} control. Additionally, if you want
  63  * a form of rich-text editing, there is also the
  64  * {@link javafx.scene.web.HTMLEditor HTMLEditor} control.
  65  *
  66  * <p>TextArea supports the notion of showing {@link #promptTextProperty() prompt text}
  67  * to the user when there is no {@link #textProperty() text} already in the
  68  * TextArea (either via the user, or set programmatically). This is a useful
  69  * way of informing the user as to what is expected in the text area, without
  70  * having to resort to {@link Tooltip tooltips} or on-screen {@link Label labels}.
  71  *
  72  * @see TextField
  73  * @since JavaFX 2.0
  74  */
  75 public class TextArea extends TextInputControl {
  76     // Text area content model
  77     private static final class TextAreaContent implements Content {
  78         private ExpressionHelper<String> helper = null;
  79         private ArrayList<StringBuilder> paragraphs = new ArrayList<StringBuilder>();
  80         private int contentLength = 0;
  81         private ParagraphList paragraphList = new ParagraphList();
  82         private ListListenerHelper<CharSequence> listenerHelper;
  83 
  84         private TextAreaContent() {
  85             paragraphs.add(new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY));
  86             paragraphList.content = this;
  87         }
  88 
  89         @Override public String get(int start, int end) {
  90             int length = end - start;
  91             StringBuilder textBuilder = new StringBuilder(length);
  92 
  93             int paragraphCount = paragraphs.size();
  94 
  95             int paragraphIndex = 0;
  96             int offset = start;
  97 
  98             while (paragraphIndex < paragraphCount) {
  99                 StringBuilder paragraph = paragraphs.get(paragraphIndex);
 100                 int count = paragraph.length() + 1;
 101 
 102                 if (offset < count) {
 103                     break;
 104                 }
 105 
 106                 offset -= count;
 107                 paragraphIndex++;
 108             }
 109 
 110             // Read characters until end is reached, appending to text builder
 111             // and moving to next paragraph as needed
 112             StringBuilder paragraph = paragraphs.get(paragraphIndex);
 113 
 114             int i = 0;
 115             while (i < length) {
 116                 if (offset == paragraph.length()
 117                     && i < contentLength) {
 118                     textBuilder.append('\n');
 119                     paragraph = paragraphs.get(++paragraphIndex);
 120                     offset = 0;
 121                 } else {
 122                     textBuilder.append(paragraph.charAt(offset++));
 123                 }
 124 
 125                 i++;
 126             }
 127 
 128             return textBuilder.toString();
 129         }
 130 
 131         @Override
 132         @SuppressWarnings("unchecked")
 133         public void insert(int index, String text, boolean notifyListeners) {
 134             if (index < 0
 135                 || index > contentLength) {
 136                 throw new IndexOutOfBoundsException();
 137             }
 138 
 139             if (text == null) {
 140                 throw new IllegalArgumentException();
 141             }
 142             text = TextInputControl.filterInput(text, false, false);
 143             int length = text.length();
 144             if (length > 0) {
 145                 // Split the text into lines
 146                 ArrayList<StringBuilder> lines = new ArrayList<StringBuilder>();
 147 
 148                 StringBuilder line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
 149                 for (int i = 0; i < length; i++) {
 150                     char c = text.charAt(i);
 151 
 152                     if (c == '\n') {
 153                         lines.add(line);
 154                         line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
 155                     } else {
 156                         line.append(c);
 157                     }
 158                 }
 159 
 160                 lines.add(line);
 161 
 162                 // Merge the text into the existing content
 163                 // Merge the text into the existing content
 164                 int paragraphIndex = paragraphs.size();
 165                 int offset = contentLength + 1;
 166 
 167                 StringBuilder paragraph = null;
 168 
 169                 do {
 170                     paragraph = paragraphs.get(--paragraphIndex);
 171                     offset -= paragraph.length() + 1;
 172                 } while (index < offset);
 173 
 174                 int start = index - offset;
 175 
 176                 int n = lines.size();
 177                 if (n == 1) {
 178                     // The text contains only a single line; insert it into the
 179                     // intersecting paragraph
 180                     paragraph.insert(start, line);
 181                     fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
 182                         Collections.singletonList((CharSequence)paragraph));
 183                 } else {
 184                     // The text contains multiple line; split the intersecting
 185                     // paragraph
 186                     int end = paragraph.length();
 187                     CharSequence trailingText = paragraph.subSequence(start, end);
 188                     paragraph.delete(start, end);
 189 
 190                     // Append the first line to the intersecting paragraph and
 191                     // append the trailing text to the last line
 192                     StringBuilder first = lines.get(0);
 193                     paragraph.insert(start, first);
 194                     line.append(trailingText);
 195                     fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
 196                         Collections.singletonList((CharSequence)paragraph));
 197 
 198                     // Insert the remaining lines into the paragraph list
 199                     paragraphs.addAll(paragraphIndex + 1, lines.subList(1, n));
 200                     fireParagraphListChangeEvent(paragraphIndex + 1, paragraphIndex + n,
 201                         Collections.EMPTY_LIST);
 202                 }
 203 
 204                 // Update content length
 205                 contentLength += length;
 206                 if (notifyListeners) {
 207                     ExpressionHelper.fireValueChangedEvent(helper);
 208                 }
 209             }
 210         }
 211 
 212         @Override public void delete(int start, int end, boolean notifyListeners) {
 213             if (start > end) {
 214                 throw new IllegalArgumentException();
 215             }
 216 
 217             if (start < 0
 218                 || end > contentLength) {
 219                 throw new IndexOutOfBoundsException();
 220             }
 221 
 222             int length = end - start;
 223 
 224             if (length > 0) {
 225                 // Identify the trailing paragraph index
 226                 int paragraphIndex = paragraphs.size();
 227                 int offset = contentLength + 1;
 228 
 229                 StringBuilder paragraph = null;
 230 
 231                 do {
 232                     paragraph = paragraphs.get(--paragraphIndex);
 233                     offset -= paragraph.length() + 1;
 234                 } while (end < offset);
 235 
 236                 int trailingParagraphIndex = paragraphIndex;
 237                 int trailingOffset = offset;
 238                 StringBuilder trailingParagraph = paragraph;
 239 
 240                 // Identify the leading paragraph index
 241                 paragraphIndex++;
 242                 offset += paragraph.length() + 1;
 243 
 244                 do {
 245                     paragraph = paragraphs.get(--paragraphIndex);
 246                     offset -= paragraph.length() + 1;
 247                 } while (start < offset);
 248 
 249                 int leadingParagraphIndex = paragraphIndex;
 250                 int leadingOffset = offset;
 251                 StringBuilder leadingParagraph = paragraph;
 252 
 253                 // Remove the text
 254                 if (leadingParagraphIndex == trailingParagraphIndex) {
 255                     // The removal affects only a single paragraph
 256                     leadingParagraph.delete(start - leadingOffset,
 257                         end - leadingOffset);
 258 
 259                     fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
 260                         Collections.singletonList((CharSequence)leadingParagraph));
 261                 } else {
 262                     // The removal spans paragraphs; remove any intervening paragraphs and
 263                     // merge the leading and trailing segments
 264                     CharSequence leadingSegment = leadingParagraph.subSequence(0,
 265                         start - leadingOffset);
 266                     int trailingSegmentLength = (start + length) - trailingOffset;
 267 
 268                     trailingParagraph.delete(0, trailingSegmentLength);
 269                     fireParagraphListChangeEvent(trailingParagraphIndex, trailingParagraphIndex + 1,
 270                         Collections.singletonList((CharSequence)trailingParagraph));
 271 
 272                     if (trailingParagraphIndex - leadingParagraphIndex > 0) {
 273                         List<CharSequence> removed = new ArrayList<CharSequence>(paragraphs.subList(leadingParagraphIndex,
 274                             trailingParagraphIndex));
 275                         paragraphs.subList(leadingParagraphIndex,
 276                             trailingParagraphIndex).clear();
 277                         fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex,
 278                             removed);
 279                     }
 280 
 281                     // Trailing paragraph is now at the former leading paragraph's index
 282                     trailingParagraph.insert(0, leadingSegment);
 283                     fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
 284                         Collections.singletonList((CharSequence)leadingParagraph));
 285                 }
 286 
 287                 // Update content length
 288                 contentLength -= length;
 289                 if (notifyListeners) {
 290                     ExpressionHelper.fireValueChangedEvent(helper);
 291                 }
 292             }
 293         }
 294 
 295         @Override public int length() {
 296             return contentLength;
 297         }
 298 
 299         @Override public String get() {
 300             return get(0, length());
 301         }
 302 
 303         @Override public void addListener(ChangeListener<? super String> changeListener) {
 304             helper = ExpressionHelper.addListener(helper, this, changeListener);
 305         }
 306 
 307         @Override public void removeListener(ChangeListener<? super String> changeListener) {
 308             helper = ExpressionHelper.removeListener(helper, changeListener);
 309         }
 310 
 311         @Override public String getValue() {
 312             return get();
 313         }
 314 
 315         @Override public void addListener(InvalidationListener listener) {
 316             helper = ExpressionHelper.addListener(helper, this, listener);
 317         }
 318 
 319         @Override public void removeListener(InvalidationListener listener) {
 320             helper = ExpressionHelper.removeListener(helper, listener);
 321         }
 322 
 323         private void fireParagraphListChangeEvent(int from, int to, List<CharSequence> removed) {
 324             ParagraphListChange change = new ParagraphListChange(paragraphList, from, to, removed);
 325             ListListenerHelper.fireValueChangedEvent(listenerHelper, change);
 326         }
 327     }
 328 
 329     // Observable list of paragraphs
 330     private static final class ParagraphList extends AbstractList<CharSequence>
 331             implements ObservableList<CharSequence> {
 332 
 333         private TextAreaContent content;
 334 
 335         @Override
 336         public CharSequence get(int index) {
 337             return content.paragraphs.get(index);
 338         }
 339 
 340         @Override
 341         public boolean addAll(Collection<? extends CharSequence> paragraphs) {
 342             throw new UnsupportedOperationException();
 343         }
 344 
 345         @Override
 346         public boolean addAll(CharSequence... paragraphs) {
 347             throw new UnsupportedOperationException();
 348         }
 349 
 350         @Override
 351         public boolean setAll(Collection<? extends CharSequence> paragraphs) {
 352             throw new UnsupportedOperationException();
 353         }
 354 
 355         @Override
 356         public boolean setAll(CharSequence... paragraphs) {
 357             throw new UnsupportedOperationException();
 358         }
 359 
 360         @Override
 361         public int size() {
 362             return content.paragraphs.size();
 363         }
 364 
 365         @Override
 366         public void addListener(ListChangeListener<? super CharSequence> listener) {
 367             content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener);
 368         }
 369 
 370         @Override
 371         public void removeListener(ListChangeListener<? super CharSequence> listener) {
 372             content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener);
 373         }
 374 
 375         @Override
 376         public boolean removeAll(CharSequence... elements) {
 377             throw new UnsupportedOperationException();
 378         }
 379 
 380         @Override
 381         public boolean retainAll(CharSequence... elements) {
 382             throw new UnsupportedOperationException();
 383         }
 384 
 385         @Override
 386         public void remove(int from, int to) {
 387             throw new UnsupportedOperationException();
 388         }
 389 
 390         @Override
 391         public void addListener(InvalidationListener listener) {
 392             content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener);
 393         }
 394 
 395         @Override
 396         public void removeListener(InvalidationListener listener) {
 397             content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener);
 398         }
 399     }
 400 
 401     private static final class ParagraphListChange extends NonIterableChange<CharSequence>  {
 402 
 403         private List<CharSequence> removed;
 404 
 405         protected ParagraphListChange(ObservableList<CharSequence> list, int from, int to,
 406             List<CharSequence> removed) {
 407             super(from, to, list);
 408 
 409             this.removed = removed;
 410         }
 411 
 412         @Override
 413         public List<CharSequence> getRemoved() {
 414             return removed;
 415         }
 416 
 417         @Override
 418         protected int[] getPermutation() {
 419             return new int[0];
 420         }
 421     };
 422 
 423     /**
 424      * The default value for {@link #prefColumnCount}.
 425      */
 426     public static final int DEFAULT_PREF_COLUMN_COUNT = 40;
 427 
 428     /**
 429      * The default value for {@link #prefRowCount}.
 430      */
 431     public static final int DEFAULT_PREF_ROW_COUNT = 10;
 432 
 433     private static final int DEFAULT_PARAGRAPH_CAPACITY = 32;
 434 
 435     /**
 436      * Creates a {@code TextArea} with empty text content.
 437      */
 438     public TextArea() {
 439         this("");
 440     }
 441 
 442     /**
 443      * Creates a {@code TextArea} with initial text content.
 444      *
 445      * @param text A string for text content.
 446      */
 447     public TextArea(String text) {
 448         super(new TextAreaContent());
 449 
 450         getStyleClass().add("text-area");
 451         setAccessibleRole(AccessibleRole.TEXT_AREA);
 452         setText(text);
 453     }
 454 
 455     @Override final void textUpdated() {
 456         setScrollTop(0);
 457         setScrollLeft(0);
 458     }
 459 
 460     /**
 461      * Returns an unmodifiable list of the character sequences that back the
 462      * text area's content.
 463      */
 464     public ObservableList<CharSequence> getParagraphs() {
 465         return ((TextAreaContent)getContent()).paragraphList;
 466     }
 467 
 468 
 469     /***************************************************************************
 470      *                                                                         *
 471      * Properties                                                              *
 472      *                                                                         *
 473      **************************************************************************/
 474 
 475     /**
 476      * If a run of text exceeds the width of the {@code TextArea},
 477      * then this variable indicates whether the text should wrap onto
 478      * another line.
 479      */
 480     private BooleanProperty wrapText = new StyleableBooleanProperty(false) {
 481         @Override public Object getBean() {
 482             return TextArea.this;
 483         }
 484 
 485         @Override public String getName() {
 486             return "wrapText";
 487         }
 488 
 489         @Override public CssMetaData<TextArea,Boolean> getCssMetaData() {
 490             return StyleableProperties.WRAP_TEXT;
 491         }
 492     };
 493     public final BooleanProperty wrapTextProperty() { return wrapText; }
 494     public final boolean isWrapText() { return wrapText.getValue(); }
 495     public final void setWrapText(boolean value) { wrapText.setValue(value); }
 496 
 497 
 498     /**
 499      * The preferred number of text columns. This is used for
 500      * calculating the {@code TextArea}'s preferred width.
 501      */
 502     private IntegerProperty prefColumnCount = new StyleableIntegerProperty(DEFAULT_PREF_COLUMN_COUNT) {
 503 
 504         private int oldValue = get();
 505 
 506         @Override
 507         protected void invalidated() {
 508             int value = get();
 509             if (value < 0) {
 510                 if (isBound()) {
 511                     unbind();
 512                 }
 513                 set(oldValue);
 514                 throw new IllegalArgumentException("value cannot be negative.");
 515             }
 516             oldValue = value;
 517         }
 518 
 519         @Override public CssMetaData<TextArea,Number> getCssMetaData() {
 520             return StyleableProperties.PREF_COLUMN_COUNT;
 521         }
 522 
 523         @Override
 524         public Object getBean() {
 525             return TextArea.this;
 526         }
 527 
 528         @Override
 529         public String getName() {
 530             return "prefColumnCount";
 531         }
 532     };
 533     public final IntegerProperty prefColumnCountProperty() { return prefColumnCount; }
 534     public final int getPrefColumnCount() { return prefColumnCount.getValue(); }
 535     public final void setPrefColumnCount(int value) { prefColumnCount.setValue(value); }
 536 
 537 
 538     /**
 539      * The preferred number of text rows. This is used for calculating
 540      * the {@code TextArea}'s preferred height.
 541      */
 542     private IntegerProperty prefRowCount = new StyleableIntegerProperty(DEFAULT_PREF_ROW_COUNT) {
 543 
 544         private int oldValue = get();
 545 
 546         @Override
 547         protected void invalidated() {
 548             int value = get();
 549             if (value < 0) {
 550                 if (isBound()) {
 551                     unbind();
 552                 }
 553                 set(oldValue);
 554                 throw new IllegalArgumentException("value cannot be negative.");
 555             }
 556 
 557             oldValue = value;
 558         }
 559 
 560         @Override public CssMetaData<TextArea,Number> getCssMetaData() {
 561             return StyleableProperties.PREF_ROW_COUNT;
 562         }
 563 
 564         @Override
 565         public Object getBean() {
 566             return TextArea.this;
 567         }
 568 
 569         @Override
 570         public String getName() {
 571             return "prefRowCount";
 572         }
 573     };
 574     public final IntegerProperty prefRowCountProperty() { return prefRowCount; }
 575     public final int getPrefRowCount() { return prefRowCount.getValue(); }
 576     public final void setPrefRowCount(int value) { prefRowCount.setValue(value); }
 577 
 578 
 579     /**
 580      * The number of pixels by which the content is vertically
 581      * scrolled.
 582      */
 583     private DoubleProperty scrollTop = new SimpleDoubleProperty(this, "scrollTop", 0);
 584     public final DoubleProperty scrollTopProperty() { return scrollTop; }
 585     public final double getScrollTop() { return scrollTop.getValue(); }
 586     public final void setScrollTop(double value) { scrollTop.setValue(value); }
 587 
 588 
 589     /**
 590      * The number of pixels by which the content is horizontally
 591      * scrolled.
 592      */
 593     private DoubleProperty scrollLeft = new SimpleDoubleProperty(this, "scrollLeft", 0);
 594     public final DoubleProperty scrollLeftProperty() { return scrollLeft; }
 595     public final double getScrollLeft() { return scrollLeft.getValue(); }
 596     public final void setScrollLeft(double value) { scrollLeft.setValue(value); }
 597 
 598 
 599     /***************************************************************************
 600      *                                                                         *
 601      * Methods                                                                 *
 602      *                                                                         *
 603      **************************************************************************/
 604 
 605     /** {@inheritDoc} */
 606     @Override protected Skin<?> createDefaultSkin() {
 607         return new TextAreaSkin(this);
 608     }
 609 
 610 
 611     /***************************************************************************
 612      *                                                                         *
 613      * Stylesheet Handling                                                     *
 614      *                                                                         *
 615      **************************************************************************/
 616 
 617     private static class StyleableProperties {
 618         private static final CssMetaData<TextArea,Number> PREF_COLUMN_COUNT =
 619             new CssMetaData<TextArea,Number>("-fx-pref-column-count",
 620                 SizeConverter.getInstance(), DEFAULT_PREF_COLUMN_COUNT) {
 621 
 622             @Override
 623             public boolean isSettable(TextArea n) {
 624                 return !n.prefColumnCount.isBound();
 625             }
 626 
 627             @Override
 628             public StyleableProperty<Number> getStyleableProperty(TextArea n) {
 629                 return (StyleableProperty<Number>)(WritableValue<Number>)n.prefColumnCountProperty();
 630             }
 631         };
 632 
 633         private static final CssMetaData<TextArea,Number> PREF_ROW_COUNT =
 634             new CssMetaData<TextArea,Number>("-fx-pref-row-count",
 635                 SizeConverter.getInstance(), DEFAULT_PREF_ROW_COUNT) {
 636 
 637             @Override
 638             public boolean isSettable(TextArea n) {
 639                 return !n.prefRowCount.isBound();
 640             }
 641 
 642             @Override
 643             public StyleableProperty<Number> getStyleableProperty(TextArea n) {
 644                 return (StyleableProperty<Number>)(WritableValue<Number>)n.prefRowCountProperty();
 645             }
 646         };
 647 
 648         private static final CssMetaData<TextArea,Boolean> WRAP_TEXT =
 649             new CssMetaData<TextArea,Boolean>("-fx-wrap-text",
 650                 StyleConverter.getBooleanConverter(), false) {
 651 
 652             @Override
 653             public boolean isSettable(TextArea n) {
 654                 return !n.wrapText.isBound();
 655             }
 656 
 657             @Override
 658             public StyleableProperty<Boolean> getStyleableProperty(TextArea n) {
 659                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.wrapTextProperty();
 660             }
 661         };
 662 
 663         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 664         static {
 665             final List<CssMetaData<? extends Styleable, ?>> styleables =
 666                 new ArrayList<CssMetaData<? extends Styleable, ?>>(TextInputControl.getClassCssMetaData());
 667             styleables.add(PREF_COLUMN_COUNT);
 668             styleables.add(PREF_ROW_COUNT);
 669             styleables.add(WRAP_TEXT);
 670             STYLEABLES = Collections.unmodifiableList(styleables);
 671         }
 672     }
 673 
 674     /**
 675      * @return The CssMetaData associated with this class, which may include the
 676      * CssMetaData of its superclasses.
 677      * @since JavaFX 8.0
 678      */
 679     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 680         return StyleableProperties.STYLEABLES;
 681     }
 682 
 683     /**
 684      * {@inheritDoc}
 685      * @since JavaFX 8.0
 686      */
 687     @Override
 688     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 689         return getClassCssMetaData();
 690     }
 691 }