javafx.scene.control.TextArea Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.StyleConverter;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableProperty;
import com.sun.javafx.collections.ListListenerHelper;
import com.sun.javafx.collections.NonIterableChange;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.skin.TextAreaSkin;
import javafx.css.Styleable;
import javafx.scene.AccessibleRole;
/**
* Text input component that allows a user to enter multiple lines of
* plain text. Unlike in previous releases of JavaFX, support for single line
* input is not available as part of the TextArea control, however this is
* the sole-purpose of the {@link TextField} control. Additionally, if you want
* a form of rich-text editing, there is also the
* {@link javafx.scene.web.HTMLEditor HTMLEditor} control.
*
* TextArea supports the notion of showing {@link #promptTextProperty() prompt text}
* to the user when there is no {@link #textProperty() text} already in the
* TextArea (either via the user, or set programmatically). This is a useful
* way of informing the user as to what is expected in the text area, without
* having to resort to {@link Tooltip tooltips} or on-screen {@link Label labels}.
*
*
Example:
*
var textArea = new TextArea("Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
* + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim "
* + "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip "
* + "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
* + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat "
* + "cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
* textArea.setWrapText(true);
*
*
*
* @see TextField
* @since JavaFX 2.0
*/
public class TextArea extends TextInputControl {
// Text area content model
private static final class TextAreaContent extends ContentBase {
private final List paragraphs = new ArrayList<>();
private final ParagraphList paragraphList = new ParagraphList();
private int contentLength = 0;
private TextAreaContent() {
paragraphs.add(new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY));
paragraphList.content = this;
}
@Override public String get(int start, int end) {
int length = end - start;
StringBuilder textBuilder = new StringBuilder(length);
int paragraphCount = paragraphs.size();
int paragraphIndex = 0;
int offset = start;
while (paragraphIndex < paragraphCount) {
StringBuilder paragraph = paragraphs.get(paragraphIndex);
int count = paragraph.length() + 1;
if (offset < count) {
break;
}
offset -= count;
paragraphIndex++;
}
// Read characters until end is reached, appending to text builder
// and moving to next paragraph as needed
StringBuilder paragraph = paragraphs.get(paragraphIndex);
int i = 0;
while (i < length) {
if (offset == paragraph.length()
&& i < contentLength) {
textBuilder.append('\n');
paragraph = paragraphs.get(++paragraphIndex);
offset = 0;
} else {
textBuilder.append(paragraph.charAt(offset++));
}
i++;
}
return textBuilder.toString();
}
@Override
@SuppressWarnings("unchecked")
public void insert(int index, String text, boolean notifyListeners) {
if (index < 0
|| index > contentLength) {
throw new IndexOutOfBoundsException();
}
if (text == null) {
throw new IllegalArgumentException();
}
text = TextInputControl.filterInput(text, false, false);
int length = text.length();
if (length > 0) {
// Split the text into lines
ArrayList lines = new ArrayList<>();
StringBuilder line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
for (int i = 0; i < length; i++) {
char c = text.charAt(i);
if (c == '\n') {
lines.add(line);
line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
} else {
line.append(c);
}
}
lines.add(line);
// Merge the text into the existing content
// Merge the text into the existing content
int paragraphIndex = paragraphs.size();
int offset = contentLength + 1;
StringBuilder paragraph = null;
do {
paragraph = paragraphs.get(--paragraphIndex);
offset -= paragraph.length() + 1;
} while (index < offset);
int start = index - offset;
int n = lines.size();
if (n == 1) {
// The text contains only a single line; insert it into the
// intersecting paragraph
paragraph.insert(start, line);
fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
Collections.singletonList((CharSequence)paragraph));
} else {
// The text contains multiple line; split the intersecting
// paragraph
int end = paragraph.length();
CharSequence trailingText = paragraph.subSequence(start, end);
paragraph.delete(start, end);
// Append the first line to the intersecting paragraph and
// append the trailing text to the last line
StringBuilder first = lines.get(0);
paragraph.insert(start, first);
line.append(trailingText);
fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
Collections.singletonList((CharSequence)paragraph));
// Insert the remaining lines into the paragraph list
paragraphs.addAll(paragraphIndex + 1, lines.subList(1, n));
fireParagraphListChangeEvent(paragraphIndex + 1, paragraphIndex + n,
Collections.EMPTY_LIST);
}
// Update content length
contentLength += length;
if (notifyListeners) {
fireValueChangedEvent();
}
}
}
@Override public void delete(int start, int end, boolean notifyListeners) {
if (start > end) {
throw new IllegalArgumentException();
}
if (start < 0
|| end > contentLength) {
throw new IndexOutOfBoundsException();
}
int length = end - start;
if (length > 0) {
// Identify the trailing paragraph index
int paragraphIndex = paragraphs.size();
int offset = contentLength + 1;
StringBuilder paragraph = null;
do {
paragraph = paragraphs.get(--paragraphIndex);
offset -= paragraph.length() + 1;
} while (end < offset);
int trailingParagraphIndex = paragraphIndex;
int trailingOffset = offset;
StringBuilder trailingParagraph = paragraph;
// Identify the leading paragraph index
paragraphIndex++;
offset += paragraph.length() + 1;
do {
paragraph = paragraphs.get(--paragraphIndex);
offset -= paragraph.length() + 1;
} while (start < offset);
int leadingParagraphIndex = paragraphIndex;
int leadingOffset = offset;
StringBuilder leadingParagraph = paragraph;
// Remove the text
if (leadingParagraphIndex == trailingParagraphIndex) {
// The removal affects only a single paragraph
leadingParagraph.delete(start - leadingOffset,
end - leadingOffset);
fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
Collections.singletonList((CharSequence)leadingParagraph));
} else {
// The removal spans paragraphs; remove any intervening paragraphs and
// merge the leading and trailing segments
CharSequence leadingSegment = leadingParagraph.subSequence(0,
start - leadingOffset);
int trailingSegmentLength = (start + length) - trailingOffset;
trailingParagraph.delete(0, trailingSegmentLength);
fireParagraphListChangeEvent(trailingParagraphIndex, trailingParagraphIndex + 1,
Collections.singletonList((CharSequence)trailingParagraph));
if (trailingParagraphIndex - leadingParagraphIndex > 0) {
List removed = new ArrayList<>(paragraphs.subList(leadingParagraphIndex,
trailingParagraphIndex));
paragraphs.subList(leadingParagraphIndex,
trailingParagraphIndex).clear();
fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex,
removed);
}
// Trailing paragraph is now at the former leading paragraph's index
trailingParagraph.insert(0, leadingSegment);
fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
Collections.singletonList((CharSequence)leadingParagraph));
}
// Update content length
contentLength -= length;
if (notifyListeners) {
fireValueChangedEvent();
}
}
}
@Override public int length() {
return contentLength;
}
@Override public String get() {
return get(0, length());
}
@Override public String getValue() {
return get();
}
private void fireParagraphListChangeEvent(int from, int to, List removed) {
ParagraphListChange change = new ParagraphListChange(paragraphList, from, to, removed);
ListListenerHelper.fireValueChangedEvent(paragraphList.listenerHelper, change);
}
}
// Observable list of paragraphs
private static final class ParagraphList extends AbstractList
implements ObservableList {
private TextAreaContent content;
private ListListenerHelper listenerHelper;
@Override
public CharSequence get(int index) {
return content.paragraphs.get(index);
}
@Override
public boolean addAll(Collection extends CharSequence> paragraphs) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(CharSequence... paragraphs) {
throw new UnsupportedOperationException();
}
@Override
public boolean setAll(Collection extends CharSequence> paragraphs) {
throw new UnsupportedOperationException();
}
@Override
public boolean setAll(CharSequence... paragraphs) {
throw new UnsupportedOperationException();
}
@Override
public int size() {
return content.paragraphs.size();
}
@Override
public void addListener(ListChangeListener super CharSequence> listener) {
listenerHelper = ListListenerHelper.addListener(listenerHelper, listener);
}
@Override
public void removeListener(ListChangeListener super CharSequence> listener) {
listenerHelper = ListListenerHelper.removeListener(listenerHelper, listener);
}
@Override
public boolean removeAll(CharSequence... elements) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(CharSequence... elements) {
throw new UnsupportedOperationException();
}
@Override
public void remove(int from, int to) {
throw new UnsupportedOperationException();
}
@Override
public void addListener(InvalidationListener listener) {
listenerHelper = ListListenerHelper.addListener(listenerHelper, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
listenerHelper = ListListenerHelper.removeListener(listenerHelper, listener);
}
}
private static final class ParagraphListChange extends NonIterableChange {
private List removed;
protected ParagraphListChange(ObservableList list, int from, int to,
List removed) {
super(from, to, list);
this.removed = removed;
}
@Override
public List getRemoved() {
return removed;
}
@Override
protected int[] getPermutation() {
return new int[0];
}
}
/**
* The default value for {@link #prefColumnCountProperty() prefColumnCount}.
*/
public static final int DEFAULT_PREF_COLUMN_COUNT = 40;
/**
* The default value for {@link #prefRowCountProperty() prefRowCount}.
*/
public static final int DEFAULT_PREF_ROW_COUNT = 10;
private static final int DEFAULT_PARAGRAPH_CAPACITY = 32;
/**
* Creates a {@code TextArea} with empty text content.
*/
public TextArea() {
this("");
}
/**
* Creates a {@code TextArea} with initial text content.
*
* @param text A string for text content.
*/
public TextArea(String text) {
super(new TextAreaContent());
getStyleClass().add("text-area");
setAccessibleRole(AccessibleRole.TEXT_AREA);
setText(text);
}
@Override final void textUpdated() {
setScrollTop(0);
setScrollLeft(0);
}
/**
* Returns an unmodifiable list of the character sequences that back the
* text area's content.
* @return an unmodifiable list of the character sequences that back the
* text area's content
*/
public ObservableList getParagraphs() {
return ((TextAreaContent)getContent()).paragraphList;
}
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* If a run of text exceeds the width of the {@code TextArea},
* then this variable indicates whether the text should wrap onto
* another line.
*/
private BooleanProperty wrapText = new StyleableBooleanProperty(false) {
@Override public Object getBean() {
return TextArea.this;
}
@Override public String getName() {
return "wrapText";
}
@Override public CssMetaData