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 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 super classes. 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 }