Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.control; 027 028import java.util.AbstractList; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.List; 033import javafx.beans.InvalidationListener; 034import javafx.beans.property.BooleanProperty; 035import javafx.beans.property.DoubleProperty; 036import javafx.beans.property.IntegerProperty; 037import javafx.beans.property.SimpleDoubleProperty; 038import javafx.beans.value.ChangeListener; 039import javafx.collections.ListChangeListener; 040import javafx.collections.ObservableList; 041import javafx.css.CssMetaData; 042import javafx.css.StyleConverter; 043import javafx.css.StyleableBooleanProperty; 044import javafx.css.StyleableIntegerProperty; 045import javafx.css.StyleableProperty; 046import com.sun.javafx.binding.ExpressionHelper; 047import com.sun.javafx.collections.ListListenerHelper; 048import com.sun.javafx.collections.NonIterableChange; 049import com.sun.javafx.css.converters.SizeConverter; 050import com.sun.javafx.scene.control.skin.TextAreaSkin; 051import javafx.css.Styleable; 052 053/** 054 * Text input component that allows a user to enter multiple lines of 055 * plain text. Unlike in previous releases of JavaFX, support for single line 056 * input is not available as part of the TextArea control, however this is 057 * the sole-purpose of the {@link TextField} control. Additionally, if you want 058 * a form of rich-text editing, there is also the 059 * {@link javafx.scene.web.HTMLEditor HTMLEditor} control. 060 * 061 * <p>TextArea supports the notion of showing {@link #promptTextProperty() prompt text} 062 * to the user when there is no {@link #textProperty() text} already in the 063 * TextArea (either via the user, or set programmatically). This is a useful 064 * way of informing the user as to what is expected in the text area, without 065 * having to resort to {@link Tooltip tooltips} or on-screen {@link Label labels}. 066 * 067 * @see TextField 068 */ 069public class TextArea extends TextInputControl { 070 // Text area content model 071 private static final class TextAreaContent implements Content { 072 private ExpressionHelper<String> helper = null; 073 private ArrayList<StringBuilder> paragraphs = new ArrayList<StringBuilder>(); 074 private int contentLength = 0; 075 private ParagraphList paragraphList = new ParagraphList(); 076 private ListListenerHelper<CharSequence> listenerHelper; 077 078 private TextAreaContent() { 079 paragraphs.add(new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY)); 080 paragraphList.content = this; 081 } 082 083 @Override public String get(int start, int end) { 084 int length = end - start; 085 StringBuilder textBuilder = new StringBuilder(length); 086 087 int paragraphCount = paragraphs.size(); 088 089 int paragraphIndex = 0; 090 int offset = start; 091 092 while (paragraphIndex < paragraphCount) { 093 StringBuilder paragraph = paragraphs.get(paragraphIndex); 094 int count = paragraph.length() + 1; 095 096 if (offset < count) { 097 break; 098 } 099 100 offset -= count; 101 paragraphIndex++; 102 } 103 104 // Read characters until end is reached, appending to text builder 105 // and moving to next paragraph as needed 106 StringBuilder paragraph = paragraphs.get(paragraphIndex); 107 108 int i = 0; 109 while (i < length) { 110 if (offset == paragraph.length() 111 && i < contentLength) { 112 textBuilder.append('\n'); 113 paragraph = paragraphs.get(++paragraphIndex); 114 offset = 0; 115 } else { 116 textBuilder.append(paragraph.charAt(offset++)); 117 } 118 119 i++; 120 } 121 122 return textBuilder.toString(); 123 } 124 125 @Override 126 @SuppressWarnings("unchecked") 127 public void insert(int index, String text, boolean notifyListeners) { 128 if (index < 0 129 || index > contentLength) { 130 throw new IndexOutOfBoundsException(); 131 } 132 133 if (text == null) { 134 throw new IllegalArgumentException(); 135 } 136 text = TextInputControl.filterInput(text, false, false); 137 int length = text.length(); 138 if (length > 0) { 139 // Split the text into lines 140 ArrayList<StringBuilder> lines = new ArrayList<StringBuilder>(); 141 142 StringBuilder line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY); 143 for (int i = 0; i < length; i++) { 144 char c = text.charAt(i); 145 146 if (c == '\n') { 147 lines.add(line); 148 line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY); 149 } else { 150 line.append(c); 151 } 152 } 153 154 lines.add(line); 155 156 // Merge the text into the existing content 157 // Merge the text into the existing content 158 int paragraphIndex = paragraphs.size(); 159 int offset = contentLength + 1; 160 161 StringBuilder paragraph = null; 162 163 do { 164 paragraph = paragraphs.get(--paragraphIndex); 165 offset -= paragraph.length() + 1; 166 } while (index < offset); 167 168 int start = index - offset; 169 170 int n = lines.size(); 171 if (n == 1) { 172 // The text contains only a single line; insert it into the 173 // intersecting paragraph 174 paragraph.insert(start, line); 175 fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1, 176 Collections.singletonList((CharSequence)paragraph)); 177 } else { 178 // The text contains multiple line; split the intersecting 179 // paragraph 180 int end = paragraph.length(); 181 CharSequence trailingText = paragraph.subSequence(start, end); 182 paragraph.delete(start, end); 183 184 // Append the first line to the intersecting paragraph and 185 // append the trailing text to the last line 186 StringBuilder first = lines.get(0); 187 paragraph.insert(start, first); 188 line.append(trailingText); 189 fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1, 190 Collections.singletonList((CharSequence)paragraph)); 191 192 // Insert the remaining lines into the paragraph list 193 paragraphs.addAll(paragraphIndex + 1, lines.subList(1, n)); 194 fireParagraphListChangeEvent(paragraphIndex + 1, paragraphIndex + n, 195 Collections.EMPTY_LIST); 196 } 197 198 // Update content length 199 contentLength += length; 200 if (notifyListeners) { 201 ExpressionHelper.fireValueChangedEvent(helper); 202 } 203 } 204 } 205 206 @Override public void delete(int start, int end, boolean notifyListeners) { 207 if (start > end) { 208 throw new IllegalArgumentException(); 209 } 210 211 if (start < 0 212 || end > contentLength) { 213 throw new IndexOutOfBoundsException(); 214 } 215 216 int length = end - start; 217 218 if (length > 0) { 219 // Identify the trailing paragraph index 220 int paragraphIndex = paragraphs.size(); 221 int offset = contentLength + 1; 222 223 StringBuilder paragraph = null; 224 225 do { 226 paragraph = paragraphs.get(--paragraphIndex); 227 offset -= paragraph.length() + 1; 228 } while (end < offset); 229 230 int trailingParagraphIndex = paragraphIndex; 231 int trailingOffset = offset; 232 StringBuilder trailingParagraph = paragraph; 233 234 // Identify the leading paragraph index 235 paragraphIndex++; 236 offset += paragraph.length() + 1; 237 238 do { 239 paragraph = paragraphs.get(--paragraphIndex); 240 offset -= paragraph.length() + 1; 241 } while (start < offset); 242 243 int leadingParagraphIndex = paragraphIndex; 244 int leadingOffset = offset; 245 StringBuilder leadingParagraph = paragraph; 246 247 // Remove the text 248 if (leadingParagraphIndex == trailingParagraphIndex) { 249 // The removal affects only a single paragraph 250 leadingParagraph.delete(start - leadingOffset, 251 end - leadingOffset); 252 253 fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1, 254 Collections.singletonList((CharSequence)leadingParagraph)); 255 } else { 256 // The removal spans paragraphs; remove any intervening paragraphs and 257 // merge the leading and trailing segments 258 CharSequence leadingSegment = leadingParagraph.subSequence(0, 259 start - leadingOffset); 260 int trailingSegmentLength = (start + length) - trailingOffset; 261 262 trailingParagraph.delete(0, trailingSegmentLength); 263 fireParagraphListChangeEvent(trailingParagraphIndex, trailingParagraphIndex + 1, 264 Collections.singletonList((CharSequence)trailingParagraph)); 265 266 if (trailingParagraphIndex - leadingParagraphIndex > 0) { 267 List<CharSequence> removed = new ArrayList<CharSequence>(paragraphs.subList(leadingParagraphIndex, 268 trailingParagraphIndex)); 269 paragraphs.subList(leadingParagraphIndex, 270 trailingParagraphIndex).clear(); 271 fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex, 272 removed); 273 } 274 275 // Trailing paragraph is now at the former leading paragraph's index 276 trailingParagraph.insert(0, leadingSegment); 277 fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1, 278 Collections.singletonList((CharSequence)leadingParagraph)); 279 } 280 281 // Update content length 282 contentLength -= length; 283 if (notifyListeners) { 284 ExpressionHelper.fireValueChangedEvent(helper); 285 } 286 } 287 } 288 289 @Override public int length() { 290 return contentLength; 291 } 292 293 @Override public String get() { 294 return get(0, length()); 295 } 296 297 @Override public void addListener(ChangeListener<? super String> changeListener) { 298 helper = ExpressionHelper.addListener(helper, this, changeListener); 299 } 300 301 @Override public void removeListener(ChangeListener<? super String> changeListener) { 302 helper = ExpressionHelper.removeListener(helper, changeListener); 303 } 304 305 @Override public String getValue() { 306 return get(); 307 } 308 309 @Override public void addListener(InvalidationListener listener) { 310 helper = ExpressionHelper.addListener(helper, this, listener); 311 } 312 313 @Override public void removeListener(InvalidationListener listener) { 314 helper = ExpressionHelper.removeListener(helper, listener); 315 } 316 317 private void fireParagraphListChangeEvent(int from, int to, List<CharSequence> removed) { 318 ParagraphListChange change = new ParagraphListChange(paragraphList, from, to, removed); 319 ListListenerHelper.fireValueChangedEvent(listenerHelper, change); 320 } 321 } 322 323 // Observable list of paragraphs 324 private static final class ParagraphList extends AbstractList<CharSequence> 325 implements ObservableList<CharSequence> { 326 327 private TextAreaContent content; 328 329 @Override 330 public CharSequence get(int index) { 331 return content.paragraphs.get(index); 332 } 333 334 @Override 335 public boolean addAll(Collection<? extends CharSequence> paragraphs) { 336 throw new UnsupportedOperationException(); 337 } 338 339 @Override 340 public boolean addAll(CharSequence... paragraphs) { 341 throw new UnsupportedOperationException(); 342 } 343 344 @Override 345 public boolean setAll(Collection<? extends CharSequence> paragraphs) { 346 throw new UnsupportedOperationException(); 347 } 348 349 @Override 350 public boolean setAll(CharSequence... paragraphs) { 351 throw new UnsupportedOperationException(); 352 } 353 354 @Override 355 public int size() { 356 return content.paragraphs.size(); 357 } 358 359 @Override 360 public void addListener(ListChangeListener<? super CharSequence> listener) { 361 content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener); 362 } 363 364 @Override 365 public void removeListener(ListChangeListener<? super CharSequence> listener) { 366 content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener); 367 } 368 369 @Override 370 public boolean removeAll(CharSequence... elements) { 371 throw new UnsupportedOperationException(); 372 } 373 374 @Override 375 public boolean retainAll(CharSequence... elements) { 376 throw new UnsupportedOperationException(); 377 } 378 379 @Override 380 public void remove(int from, int to) { 381 throw new UnsupportedOperationException(); 382 } 383 384 @Override 385 public void addListener(InvalidationListener listener) { 386 content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener); 387 } 388 389 @Override 390 public void removeListener(InvalidationListener listener) { 391 content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener); 392 } 393 } 394 395 private static final class ParagraphListChange extends NonIterableChange<CharSequence> { 396 397 private List<CharSequence> removed; 398 399 protected ParagraphListChange(ObservableList<CharSequence> list, int from, int to, 400 List<CharSequence> removed) { 401 super(from, to, list); 402 403 this.removed = removed; 404 } 405 406 @Override 407 public List<CharSequence> getRemoved() { 408 return removed; 409 } 410 411 @Override 412 protected int[] getPermutation() { 413 return new int[0]; 414 } 415 }; 416 417 /** 418 * The default value for {@link #prefColumnCount}. 419 */ 420 public static final int DEFAULT_PREF_COLUMN_COUNT = 40; 421 422 /** 423 * The default value for {@link #prefRowCount}. 424 */ 425 public static final int DEFAULT_PREF_ROW_COUNT = 10; 426 427 /** 428 * @treatAsPrivate implementation detail 429 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 430 */ 431 public static final int DEFAULT_PARAGRAPH_CAPACITY = 32; 432 433 /** 434 * Creates a {@code TextArea} with empty text content. 435 */ 436 public TextArea() { 437 this(""); 438 } 439 440 /** 441 * Creates a {@code TextArea} with initial text content. 442 * 443 * @param text A string for text content. 444 */ 445 public TextArea(String text) { 446 super(new TextAreaContent()); 447 448 getStyleClass().add("text-area"); 449 setText(text); 450 } 451 452 @Override final void textUpdated() { 453 setScrollTop(0); 454 setScrollLeft(0); 455 } 456 457 /** 458 * Returns an unmodifiable list of the character sequences that back the 459 * text area's content. 460 */ 461 public ObservableList<CharSequence> getParagraphs() { 462 return ((TextAreaContent)getContent()).paragraphList; 463 } 464 465 466 /*************************************************************************** 467 * * 468 * Properties * 469 * * 470 **************************************************************************/ 471 472 /** 473 * If a run of text exceeds the width of the {@code TextArea}, 474 * then this variable indicates whether the text should wrap onto 475 * another line. 476 */ 477 private BooleanProperty wrapText = new StyleableBooleanProperty(false) { 478 @Override public Object getBean() { 479 return TextArea.this; 480 } 481 482 @Override public String getName() { 483 return "wrapText"; 484 } 485 486 @Override public CssMetaData<TextArea,Boolean> getCssMetaData() { 487 return StyleableProperties.WRAP_TEXT; 488 } 489 }; 490 public final BooleanProperty wrapTextProperty() { return wrapText; } 491 public final boolean isWrapText() { return wrapText.getValue(); } 492 public final void setWrapText(boolean value) { wrapText.setValue(value); } 493 494 495 /** 496 * The preferred number of text columns. This is used for 497 * calculating the {@code TextArea}'s preferred width. 498 */ 499 private IntegerProperty prefColumnCount = new StyleableIntegerProperty(DEFAULT_PREF_COLUMN_COUNT) { 500 @Override 501 public void set(int value) { 502 if (value < 0) { 503 throw new IllegalArgumentException("value cannot be negative."); 504 } 505 506 super.set(value); 507 } 508 509 @Override public CssMetaData<TextArea,Number> getCssMetaData() { 510 return StyleableProperties.PREF_COLUMN_COUNT; 511 } 512 513 @Override 514 public Object getBean() { 515 return TextArea.this; 516 } 517 518 @Override 519 public String getName() { 520 return "prefColumnCount"; 521 } 522 }; 523 public final IntegerProperty prefColumnCountProperty() { return prefColumnCount; } 524 public final int getPrefColumnCount() { return prefColumnCount.getValue(); } 525 public final void setPrefColumnCount(int value) { prefColumnCount.setValue(value); } 526 527 528 /** 529 * The preferred number of text rows. This is used for calculating 530 * the {@code TextArea}'s preferred height. 531 */ 532 private IntegerProperty prefRowCount = new StyleableIntegerProperty(DEFAULT_PREF_ROW_COUNT) { 533 @Override 534 public void set(int value) { 535 if (value < 0) { 536 throw new IllegalArgumentException("value cannot be negative."); 537 } 538 539 super.set(value); 540 } 541 542 @Override public CssMetaData<TextArea,Number> getCssMetaData() { 543 return StyleableProperties.PREF_ROW_COUNT; 544 } 545 546 @Override 547 public Object getBean() { 548 return TextArea.this; 549 } 550 551 @Override 552 public String getName() { 553 return "prefRowCount"; 554 } 555 }; 556 public final IntegerProperty prefRowCountProperty() { return prefRowCount; } 557 public final int getPrefRowCount() { return prefRowCount.getValue(); } 558 public final void setPrefRowCount(int value) { prefRowCount.setValue(value); } 559 560 561 /** 562 * The number of pixels by which the content is vertically 563 * scrolled. 564 */ 565 private DoubleProperty scrollTop = new SimpleDoubleProperty(this, "scrollTop", 0); 566 public final DoubleProperty scrollTopProperty() { return scrollTop; } 567 public final double getScrollTop() { return scrollTop.getValue(); } 568 public final void setScrollTop(double value) { scrollTop.setValue(value); } 569 570 571 /** 572 * The number of pixels by which the content is horizontally 573 * scrolled. 574 */ 575 private DoubleProperty scrollLeft = new SimpleDoubleProperty(this, "scrollLeft", 0); 576 public final DoubleProperty scrollLeftProperty() { return scrollLeft; } 577 public final double getScrollLeft() { return scrollLeft.getValue(); } 578 public final void setScrollLeft(double value) { scrollLeft.setValue(value); } 579 580 /*************************************************************************** 581 * * 582 * Methods * 583 * * 584 **************************************************************************/ 585 586 /** {@inheritDoc} */ 587 @Override protected Skin<?> createDefaultSkin() { 588 return new TextAreaSkin(this); 589 } 590 591 /*************************************************************************** 592 * * 593 * Stylesheet Handling * 594 * * 595 **************************************************************************/ 596 597 /** 598 * @treatAsPrivate implementation detail 599 */ 600 private static class StyleableProperties { 601 private static final CssMetaData<TextArea,Number> PREF_COLUMN_COUNT = 602 new CssMetaData<TextArea,Number>("-fx-pref-column-count", 603 SizeConverter.getInstance(), DEFAULT_PREF_COLUMN_COUNT) { 604 605 @Override 606 public boolean isSettable(TextArea n) { 607 return !n.prefColumnCount.isBound(); 608 } 609 610 @Override 611 public StyleableProperty<Number> getStyleableProperty(TextArea n) { 612 return (StyleableProperty<Number>)n.prefColumnCountProperty(); 613 } 614 }; 615 616 private static final CssMetaData<TextArea,Number> PREF_ROW_COUNT = 617 new CssMetaData<TextArea,Number>("-fx-pref-row-count", 618 SizeConverter.getInstance(), DEFAULT_PREF_ROW_COUNT) { 619 620 @Override 621 public boolean isSettable(TextArea n) { 622 return !n.prefRowCount.isBound(); 623 } 624 625 @Override 626 public StyleableProperty<Number> getStyleableProperty(TextArea n) { 627 return (StyleableProperty<Number>)n.prefRowCountProperty(); 628 } 629 }; 630 631 private static final CssMetaData<TextArea,Boolean> WRAP_TEXT = 632 new CssMetaData<TextArea,Boolean>("-fx-wrap-text", 633 StyleConverter.getBooleanConverter(), false) { 634 635 @Override 636 public boolean isSettable(TextArea n) { 637 return !n.wrapText.isBound(); 638 } 639 640 @Override 641 public StyleableProperty<Boolean> getStyleableProperty(TextArea n) { 642 return (StyleableProperty<Boolean>)n.wrapTextProperty(); 643 } 644 }; 645 646 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 647 static { 648 final List<CssMetaData<? extends Styleable, ?>> styleables = 649 new ArrayList<CssMetaData<? extends Styleable, ?>>(TextInputControl.getClassCssMetaData()); 650 styleables.add(PREF_COLUMN_COUNT); 651 styleables.add(PREF_ROW_COUNT); 652 styleables.add(WRAP_TEXT); 653 STYLEABLES = Collections.unmodifiableList(styleables); 654 } 655 } 656 657 /** 658 * @return The CssMetaData associated with this class, which may include the 659 * CssMetaData of its super classes. 660 */ 661 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 662 return StyleableProperties.STYLEABLES; 663 } 664 665 /** 666 * {@inheritDoc} 667 */ 668 @Override 669 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 670 return getClassCssMetaData(); 671 } 672}