1 /*
   2  * Copyright (c) 2011, 2015, 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     /**
 434      * @treatAsPrivate implementation detail
 435      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 436      */
 437     public static final int DEFAULT_PARAGRAPH_CAPACITY = 32;
 438 
 439     /**
 440      * Creates a {@code TextArea} with empty text content.
 441      */
 442     public TextArea() {
 443         this("");
 444     }
 445 
 446     /**
 447      * Creates a {@code TextArea} with initial text content.
 448      *
 449      * @param text A string for text content.
 450      */
 451     public TextArea(String text) {
 452         super(new TextAreaContent());
 453 
 454         getStyleClass().add("text-area");
 455         setAccessibleRole(AccessibleRole.TEXT_AREA);
 456         setText(text);
 457     }
 458 
 459     @Override final void textUpdated() {
 460         setScrollTop(0);
 461         setScrollLeft(0);
 462     }
 463 
 464     /**
 465      * Returns an unmodifiable list of the character sequences that back the
 466      * text area's content.
 467      */
 468     public ObservableList<CharSequence> getParagraphs() {
 469         return ((TextAreaContent)getContent()).paragraphList;
 470     }
 471 
 472 
 473     /***************************************************************************
 474      *                                                                         *
 475      * Properties                                                              *
 476      *                                                                         *
 477      **************************************************************************/
 478 
 479     /**
 480      * If a run of text exceeds the width of the {@code TextArea},
 481      * then this variable indicates whether the text should wrap onto
 482      * another line.
 483      */
 484     private BooleanProperty wrapText = new StyleableBooleanProperty(false) {
 485         @Override public Object getBean() {
 486             return TextArea.this;
 487         }
 488 
 489         @Override public String getName() {
 490             return "wrapText";
 491         }
 492 
 493         @Override public CssMetaData<TextArea,Boolean> getCssMetaData() {
 494             return StyleableProperties.WRAP_TEXT;
 495         }
 496     };
 497     public final BooleanProperty wrapTextProperty() { return wrapText; }
 498     public final boolean isWrapText() { return wrapText.getValue(); }
 499     public final void setWrapText(boolean value) { wrapText.setValue(value); }
 500 
 501 
 502     /**
 503      * The preferred number of text columns. This is used for
 504      * calculating the {@code TextArea}'s preferred width.
 505      */
 506     private IntegerProperty prefColumnCount = new StyleableIntegerProperty(DEFAULT_PREF_COLUMN_COUNT) {
 507 
 508         private int oldValue = get();
 509 
 510         @Override
 511         protected void invalidated() {
 512             int value = get();
 513             if (value < 0) {
 514                 if (isBound()) {
 515                     unbind();
 516                 }
 517                 set(oldValue);
 518                 throw new IllegalArgumentException("value cannot be negative.");
 519             }
 520             oldValue = value;
 521         }
 522 
 523         @Override public CssMetaData<TextArea,Number> getCssMetaData() {
 524             return StyleableProperties.PREF_COLUMN_COUNT;
 525         }
 526 
 527         @Override
 528         public Object getBean() {
 529             return TextArea.this;
 530         }
 531 
 532         @Override
 533         public String getName() {
 534             return "prefColumnCount";
 535         }
 536     };
 537     public final IntegerProperty prefColumnCountProperty() { return prefColumnCount; }
 538     public final int getPrefColumnCount() { return prefColumnCount.getValue(); }
 539     public final void setPrefColumnCount(int value) { prefColumnCount.setValue(value); }
 540 
 541 
 542     /**
 543      * The preferred number of text rows. This is used for calculating
 544      * the {@code TextArea}'s preferred height.
 545      */
 546     private IntegerProperty prefRowCount = new StyleableIntegerProperty(DEFAULT_PREF_ROW_COUNT) {
 547 
 548         private int oldValue = get();
 549 
 550         @Override
 551         protected void invalidated() {
 552             int value = get();
 553             if (value < 0) {
 554                 if (isBound()) {
 555                     unbind();
 556                 }
 557                 set(oldValue);
 558                 throw new IllegalArgumentException("value cannot be negative.");
 559             }
 560 
 561             oldValue = value;
 562         }
 563 
 564         @Override public CssMetaData<TextArea,Number> getCssMetaData() {
 565             return StyleableProperties.PREF_ROW_COUNT;
 566         }
 567 
 568         @Override
 569         public Object getBean() {
 570             return TextArea.this;
 571         }
 572 
 573         @Override
 574         public String getName() {
 575             return "prefRowCount";
 576         }
 577     };
 578     public final IntegerProperty prefRowCountProperty() { return prefRowCount; }
 579     public final int getPrefRowCount() { return prefRowCount.getValue(); }
 580     public final void setPrefRowCount(int value) { prefRowCount.setValue(value); }
 581 
 582 
 583     /**
 584      * The number of pixels by which the content is vertically
 585      * scrolled.
 586      */
 587     private DoubleProperty scrollTop = new SimpleDoubleProperty(this, "scrollTop", 0);
 588     public final DoubleProperty scrollTopProperty() { return scrollTop; }
 589     public final double getScrollTop() { return scrollTop.getValue(); }
 590     public final void setScrollTop(double value) { scrollTop.setValue(value); }
 591 
 592 
 593     /**
 594      * The number of pixels by which the content is horizontally
 595      * scrolled.
 596      */
 597     private DoubleProperty scrollLeft = new SimpleDoubleProperty(this, "scrollLeft", 0);
 598     public final DoubleProperty scrollLeftProperty() { return scrollLeft; }
 599     public final double getScrollLeft() { return scrollLeft.getValue(); }
 600     public final void setScrollLeft(double value) { scrollLeft.setValue(value); }
 601 
 602 
 603     /***************************************************************************
 604      *                                                                         *
 605      * Methods                                                                 *
 606      *                                                                         *
 607      **************************************************************************/
 608 
 609     /** {@inheritDoc} */
 610     @Override protected Skin<?> createDefaultSkin() {
 611         return new TextAreaSkin(this);
 612     }
 613 
 614 
 615     /***************************************************************************
 616      *                                                                         *
 617      * Stylesheet Handling                                                     *
 618      *                                                                         *
 619      **************************************************************************/
 620 
 621     private static class StyleableProperties {
 622         private static final CssMetaData<TextArea,Number> PREF_COLUMN_COUNT =
 623             new CssMetaData<TextArea,Number>("-fx-pref-column-count",
 624                 SizeConverter.getInstance(), DEFAULT_PREF_COLUMN_COUNT) {
 625 
 626             @Override
 627             public boolean isSettable(TextArea n) {
 628                 return !n.prefColumnCount.isBound();
 629             }
 630 
 631             @Override
 632             public StyleableProperty<Number> getStyleableProperty(TextArea n) {
 633                 return (StyleableProperty<Number>)(WritableValue<Number>)n.prefColumnCountProperty();
 634             }
 635         };
 636 
 637         private static final CssMetaData<TextArea,Number> PREF_ROW_COUNT =
 638             new CssMetaData<TextArea,Number>("-fx-pref-row-count",
 639                 SizeConverter.getInstance(), DEFAULT_PREF_ROW_COUNT) {
 640 
 641             @Override
 642             public boolean isSettable(TextArea n) {
 643                 return !n.prefRowCount.isBound();
 644             }
 645 
 646             @Override
 647             public StyleableProperty<Number> getStyleableProperty(TextArea n) {
 648                 return (StyleableProperty<Number>)(WritableValue<Number>)n.prefRowCountProperty();
 649             }
 650         };
 651 
 652         private static final CssMetaData<TextArea,Boolean> WRAP_TEXT =
 653             new CssMetaData<TextArea,Boolean>("-fx-wrap-text",
 654                 StyleConverter.getBooleanConverter(), false) {
 655 
 656             @Override
 657             public boolean isSettable(TextArea n) {
 658                 return !n.wrapText.isBound();
 659             }
 660 
 661             @Override
 662             public StyleableProperty<Boolean> getStyleableProperty(TextArea n) {
 663                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.wrapTextProperty();
 664             }
 665         };
 666 
 667         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 668         static {
 669             final List<CssMetaData<? extends Styleable, ?>> styleables =
 670                 new ArrayList<CssMetaData<? extends Styleable, ?>>(TextInputControl.getClassCssMetaData());
 671             styleables.add(PREF_COLUMN_COUNT);
 672             styleables.add(PREF_ROW_COUNT);
 673             styleables.add(WRAP_TEXT);
 674             STYLEABLES = Collections.unmodifiableList(styleables);
 675         }
 676     }
 677 
 678     /**
 679      * @return The CssMetaData associated with this class, which may include the
 680      * CssMetaData of its super classes.
 681      * @since JavaFX 8.0
 682      */
 683     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 684         return StyleableProperties.STYLEABLES;
 685     }
 686 
 687     /**
 688      * {@inheritDoc}
 689      * @since JavaFX 8.0
 690      */
 691     @Override
 692     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 693         return getClassCssMetaData();
 694     }
 695 }