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