blob: 0e6ee4488283e0c12836f230f8172799428fca79 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import org.apache.pivot.beans.DefaultProperty;
import org.apache.pivot.collections.LinkedList;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Utils;
import org.apache.pivot.wtk.Span;
import org.apache.pivot.wtk.media.Image;
import org.apache.pivot.wtk.text.Block;
import org.apache.pivot.wtk.text.ComponentNode;
import org.apache.pivot.wtk.text.ComponentNodeListener;
import org.apache.pivot.wtk.text.Document;
import org.apache.pivot.wtk.text.Element;
import org.apache.pivot.wtk.text.ImageNode;
import org.apache.pivot.wtk.text.Node;
import org.apache.pivot.wtk.text.NodeListener;
import org.apache.pivot.wtk.text.Paragraph;
import org.apache.pivot.wtk.text.PlainTextSerializer;
import org.apache.pivot.wtk.text.TextNode;
/**
* Component that allows a user to enter and edit multiple lines of (optionally
* formatted) text.
*/
@DefaultProperty("document")
public class TextPane extends Container {
/**
* Enum representing a scroll direction.
*/
public enum ScrollDirection {
UP, DOWN
}
/**
* Text pane skin interface. Text pane skins are required to implement this.
*/
public interface Skin {
/**
* Returns the insertion point for a given location.
*
* @param x The X-coordinate of the location to check.
* @param y The Y-coordinate of the location.
* @return The insertion point for the given location.
*/
public int getInsertionPoint(int x, int y);
/**
* Returns the next insertion point given an x coordinate and a
* character offset.
*
* @param x The current X-coordinate to move from.
* @param from The current character offset to move from.
* @param direction The direction to move from the current location.
* @return The next insertion point.
*/
public int getNextInsertionPoint(int x, int from, ScrollDirection direction);
/**
* Returns the row index of the character at a given offset within the
* document.
*
* @param offset The character offset to check.
* @return The row index of the character at the given offset.
*/
public int getRowAt(int offset);
/**
* @return The total number of rows in the document.
*/
public int getRowCount();
/**
* Returns the bounds of the character at a given offset within the
* document.
*
* @param offset The index of the character we want the bounds for.
* @return The bounds of the character at the given offset.
*/
public Bounds getCharacterBounds(int offset);
/**
* @return The current setting of the "tabWidth" style (so "setText"
* uses the same value as Ctrl-Tab from user).
*/
public int getTabWidth();
}
private interface Edit {
public void undo();
}
private static class RangeRemovedEdit implements Edit {
private final Node node;
private final int offset;
private final Sequence<Node> removed;
public RangeRemovedEdit(Node node, Sequence<Node> removed, int offset) {
this.node = node;
this.offset = offset;
this.removed = removed;
}
@Override
public void undo() {
Document tmp = new Document();
for (int i = 0; i < removed.getLength(); i++) {
tmp.add(removed.get(i));
}
node.insertRange(tmp, offset);
}
}
private class RangeInsertedEdit implements Edit {
private final Node node;
private final int offset;
private final int characterCount;
public RangeInsertedEdit(Node node, int offset, int characterCount) {
this.node = node;
this.offset = offset;
this.characterCount = characterCount;
}
@Override
public void undo() {
node.removeRange(offset, characterCount);
int newSelectionStart = selectionStart;
int newSelectionLength = selectionLength;
if (newSelectionStart >= document.getCharacterCount()) {
newSelectionStart = document.getCharacterCount() - 1;
}
if (newSelectionStart + newSelectionLength > document.getCharacterCount()) {
newSelectionLength = document.getCharacterCount() - newSelectionStart;
}
setSelection(newSelectionStart, newSelectionLength);
}
}
private static class TextPaneListenerList extends WTKListenerList<TextPaneListener> implements
TextPaneListener {
@Override
public void documentChanged(TextPane textPane, Document previousText) {
for (TextPaneListener listener : this) {
listener.documentChanged(textPane, previousText);
}
}
@Override
public void editableChanged(TextPane textPane) {
for (TextPaneListener listener : this) {
listener.editableChanged(textPane);
}
}
}
private static class TextPaneCharacterListenerList extends
WTKListenerList<TextPaneCharacterListener> implements TextPaneCharacterListener {
/**
* @param index Index into the whole document.
*/
@Override
public void charactersInserted(TextPane textPane, int index, int count) {
for (TextPaneCharacterListener listener : this) {
listener.charactersInserted(textPane, index, count);
}
}
/**
* @param index Index into the whole document.
*/
@Override
public void charactersRemoved(TextPane textPane, int index, int count) {
for (TextPaneCharacterListener listener : this) {
listener.charactersRemoved(textPane, index, count);
}
}
}
private static class TextPaneSelectionListenerList extends
WTKListenerList<TextPaneSelectionListener> implements TextPaneSelectionListener {
@Override
public void selectionChanged(TextPane textPane, int previousSelectionStart,
int previousSelectionLength) {
for (TextPaneSelectionListener listener : this) {
listener.selectionChanged(textPane, previousSelectionStart, previousSelectionLength);
}
}
}
private Document document = null;
private int selectionStart = 0;
private int selectionLength = 0;
private boolean expandTabs = false;
private boolean editable = true;
private boolean undoingHistory = false;
private boolean bulkOperation = false;
private ComponentNodeListener componentNodeListener = new ComponentNodeListener() {
@Override
public void componentChanged(ComponentNode componentNode, Component previousComponent) {
// @TODO need to insert this at the correct index
TextPane.super.remove(previousComponent);
TextPane.super.add(componentNode.getComponent());
}
};
private NodeListener documentListener = new NodeListener.Adapter() {
/**
* @param offset Offset into the document.
*/
@Override
public void rangeInserted(Node node, int offset, int characterCount) {
if (selectionStart + selectionLength > offset) {
if (selectionStart > offset) {
selectionStart += characterCount;
} else {
selectionLength += characterCount;
}
}
if (!undoingHistory) {
addHistoryItem(new RangeInsertedEdit(node, offset, characterCount));
}
if (!bulkOperation) {
textPaneCharacterListeners.charactersInserted(TextPane.this, offset, characterCount);
}
}
/**
* @param offset Offset into the document.
*/
@Override
public void nodesRemoved(Node node, Sequence<Node> removed, int offset) {
for (int i = 0; i < removed.getLength(); i++) {
Node descendant = removed.get(i);
if (descendant instanceof ComponentNode) {
ComponentNode componentNode = (ComponentNode) descendant;
componentNode.getComponentNodeListeners().remove(componentNodeListener);
TextPane.super.remove(componentNode.getComponent());
}
}
if (!undoingHistory) {
addHistoryItem(new RangeRemovedEdit(node, removed, offset));
}
}
/**
* @param offset Offset into the document.
*/
@Override
public void nodeInserted(Node node, int offset) {
Node descendant = document.getDescendantAt(offset);
if (descendant instanceof ComponentNode) {
ComponentNode componentNode = (ComponentNode) descendant;
componentNode.getComponentNodeListeners().add(componentNodeListener);
TextPane.super.add(componentNode.getComponent());
}
}
/**
* @param offset Offset into the document.
*/
@Override
public void rangeRemoved(Node node, int offset, int characterCount) {
// if the end of the selection is in or after the range removed
if (selectionStart + selectionLength > offset) {
// if the start of the selection is in the range removed
if (selectionStart > offset) {
selectionStart -= characterCount;
if (selectionStart < offset) {
selectionStart = offset;
}
} else {
selectionLength -= characterCount;
if (selectionLength < 0) {
selectionLength = 0;
}
}
}
if (!bulkOperation) {
textPaneCharacterListeners.charactersRemoved(TextPane.this, offset, characterCount);
}
}
};
private LinkedList<Edit> editHistory = new LinkedList<>();
private TextPaneListenerList textPaneListeners = new TextPaneListenerList();
private TextPaneCharacterListenerList textPaneCharacterListeners = new TextPaneCharacterListenerList();
private TextPaneSelectionListenerList textPaneSelectionListeners = new TextPaneSelectionListenerList();
private static final int MAXIMUM_EDIT_HISTORY_LENGTH = 30;
public TextPane() {
installSkin(TextPane.class);
}
@Override
protected void setSkin(org.apache.pivot.wtk.Skin skin) {
if (!(skin instanceof TextPane.Skin)) {
throw new IllegalArgumentException("Skin class must implement "
+ TextPane.Skin.class.getName());
}
super.setSkin(skin);
}
/**
* @return The document that backs the text pane.
*/
public Document getDocument() {
return document;
}
private void checkDocumentExists() {
if (document == null || document.getCharacterCount() == 0) {
throw new IllegalStateException("document is null or empty.");
}
}
/**
* Sets the document that backs the text pane. Documents are not shareable
* across multiple TextPanes; because a Document may contain Components, and
* a Component may only be in one Container at a time.
*
* @param document The new document to be displayed by this text pane.
*/
public void setDocument(Document document) {
Document previousDocument = this.document;
if (previousDocument != document) {
if (previousDocument != null) {
previousDocument.getNodeListeners().remove(documentListener);
removeComponentNodes(previousDocument);
}
if (document != null) {
document.getNodeListeners().add(documentListener);
addComponentNodes(document);
}
// Clear the edit history
editHistory.clear();
this.document = document;
selectionStart = 0;
selectionLength = 0;
textPaneListeners.documentChanged(this, previousDocument);
}
}
private void removeComponentNodes(Element element) {
for (Node childNode : element) {
if (childNode instanceof Element) {
removeComponentNodes((Element) childNode);
}
if (childNode instanceof ComponentNode) {
remove(((ComponentNode) childNode).getComponent());
}
}
}
private void addComponentNodes(Element element) {
for (Node childNode : element) {
if (childNode instanceof Element) {
addComponentNodes((Element) childNode);
}
if (childNode instanceof ComponentNode) {
add(((ComponentNode) childNode).getComponent());
}
}
}
private Node getRightmostDescendant(Element element) {
int n = element.getLength();
if (n > 0) {
Node node = element.get(n - 1);
if (node instanceof Element) {
return getRightmostDescendant((Element) node);
}
return node;
}
return element;
}
/**
* Helper function to remove a range of characters from the document and
* notify the listeners just once (instead of once per node).
*
* @param start The starting location (document offset) of the characters
* to be removed.
* @param count The number of characters to remove from that location.
* @return The document node where the characters were removed.
*/
private Node removeDocumentRange(int start, int count) {
bulkOperation = true;
Node node = document.removeRange(start, count);
bulkOperation = false;
textPaneCharacterListeners.charactersRemoved(this, start, count);
return node;
}
public void insert(char character) {
// TODO Don't make every character undoable; break at word boundaries?
insert(Character.toString(character));
}
public void insert(String text) {
if (selectionLength > 0) {
delete(false);
}
insertText(text, selectionStart);
}
public void insertText(String text, int index) {
Utils.checkNull(text, "text");
if (document == null) {
throw new IllegalStateException("document is null.");
}
if (document.getCharacterCount() == 0) {
// the document is currently empty
document.insert(new Paragraph(text), 0);
} else {
Node descendant = document.getDescendantAt(index);
int offset = index - descendant.getDocumentOffset();
if (descendant instanceof TextNode) {
// The caret is positioned within an existing text node
TextNode textNode = (TextNode) descendant;
textNode.insertText(text, offset);
} else if (descendant instanceof Paragraph) {
// The caret is positioned on the paragraph terminator
// so get to the bottom rightmost descendant and add there
Paragraph paragraph = (Paragraph) descendant;
Node node = getRightmostDescendant(paragraph);
if (node instanceof TextNode) {
// Insert the text into the existing node
TextNode textNode = (TextNode) node;
textNode.insertText(text, index - textNode.getDocumentOffset());
} else if (node instanceof Element) {
// Append a new text node
Element element = (Element) node;
element.add(new TextNode(text));
} else {
// The paragraph is currently empty
paragraph.add(new TextNode(text));
}
} else {
// The caret is positioned on a non-text character node; insert
// the text into the descendant's parent
Element parent = descendant.getParent();
int elemIndex = parent.indexOf(descendant);
parent.insert(new TextNode(text), elemIndex);
}
}
// Set the selection start to the character following the insertion
setSelection(index + text.length(), 0);
}
public void insertImage(Image image) {
Utils.checkNull(image, "image");
checkDocumentExists();
if (selectionLength > 0) {
removeDocumentRange(selectionStart, selectionLength);
}
// TODO If the caret is placed in the middle of a text node, split it;
// otherwise, insert an ImageNode immediately following the block
// containing the caret
// If the insertion is at the end of the document, then just add
if (selectionStart >= document.getCharacterCount() - 1) {
document.add(new ImageNode(image));
} else {
// Walk up the tree until we find a block
Node descendant = document.getDescendantAt(selectionStart);
while (!(descendant instanceof Block)) {
descendant = descendant.getParent();
}
Element parent = descendant.getParent();
if (parent != null) {
int index = parent.indexOf(descendant);
parent.insert(new ImageNode(image), index + 1);
}
}
// Set the selection start to the character following the insertion
setSelection(selectionStart + 1, selectionLength);
}
public void insertComponent(Component component) {
Utils.checkNull(component, "component");
checkDocumentExists();
if (selectionLength > 0) {
removeDocumentRange(selectionStart, selectionLength);
}
// TODO If the caret is placed in the middle of a text node, split it;
// otherwise, insert a ComponentNode immediately following the block
// containing the caret
// If the insertion is at the end of the document, then just add
if (selectionStart >= document.getCharacterCount() - 1) {
document.add(new ComponentNode(component));
} else {
// Walk up the tree until we find a block
Node descendant = document.getDescendantAt(selectionStart);
while (!(descendant instanceof Block)) {
descendant = descendant.getParent();
}
Element parent = descendant.getParent();
if (parent != null) {
int index = parent.indexOf(descendant);
parent.insert(new ComponentNode(component), index + 1);
}
}
// Set the selection start to the character following the insertion
setSelection(selectionStart + 1, selectionLength);
}
public void insertParagraph() {
checkDocumentExists();
if (selectionLength > 0) {
removeDocumentRange(selectionStart, selectionLength);
}
// Walk up the tree until we find a paragraph
Node descendant = document.getDescendantAt(selectionStart);
while (!(descendant instanceof Paragraph)) {
descendant = descendant.getParent();
}
// Split the paragraph at the insertion point
Paragraph leadingSegment = (Paragraph) descendant;
int offset = selectionStart - leadingSegment.getDocumentOffset();
int characterCount = leadingSegment.getCharacterCount() - offset;
Paragraph trailingSegment = (Paragraph) leadingSegment.removeRange(offset, characterCount);
Element parent = leadingSegment.getParent();
int index = parent.indexOf(leadingSegment);
parent.insert(trailingSegment, index + 1);
// Set the selection start to the character following the insertion
setSelection(selectionStart + 1, selectionLength);
}
/**
* Returns character count of the document.
*
* @return The document's character count, or <tt>0</tt> if the document is
* <tt>null</tt>.
*/
public int getCharacterCount() {
return (document == null) ? 0 : document.getCharacterCount();
}
public void delete(boolean backspace) {
if (selectionLength > 0) {
removeText(selectionStart, selectionLength);
} else {
if (backspace) {
removeText(selectionStart - 1, 1);
} else {
removeText(selectionStart, 1);
}
}
}
public void removeText(int offset, int characterCount) {
checkDocumentExists();
if (offset >= 0 && offset < document.getCharacterCount()) {
Node descendant = document.getDescendantAt(offset);
// Used to be: if (selectionLength == 0 && ...
if (characterCount == 0 && descendant instanceof Paragraph) {
// We are deleting a paragraph terminator
Paragraph paragraph = (Paragraph) descendant;
Element parent = paragraph.getParent();
int index = parent.indexOf(paragraph);
// Attempt to merge any successive content into the paragraph
if (index < parent.getLength() - 1) {
// TODO This won't always be a paragraph - we'll need to
// find the next paragraph by walking the tree, then
// remove any empty nodes
Sequence<Node> removed = parent.remove(index + 1, 1);
Paragraph nextParagraph = (Paragraph) removed.get(0);
paragraph.insertRange(nextParagraph, paragraph.getCharacterCount() - 1);
}
} else {
removeDocumentRange(offset, characterCount);
}
}
// Ensure that the document remains editable
if (document.getCharacterCount() == 0) {
document.add(new Paragraph(""));
}
// Move the caret to the merge point
if (offset >= 0) {
setSelection(offset, 0);
}
}
public void cut() {
checkDocumentExists();
if (selectionLength > 0) {
// Copy selection to clipboard
Document selection = (Document) removeDocumentRange(selectionStart, selectionLength);
String selectedText = null;
try {
PlainTextSerializer serializer = new PlainTextSerializer();
StringWriter writer = new StringWriter();
serializer.writeObject(selection, writer);
selectedText = writer.toString();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
if (selectedText != null) {
LocalManifest clipboardContent = new LocalManifest();
clipboardContent.putText(selectedText);
Clipboard.setContent(clipboardContent);
}
}
setSelection(selectionStart, 0);
}
public void copy() {
checkDocumentExists();
String selectedText = getSelectedText();
if (selectedText != null) {
LocalManifest clipboardContent = new LocalManifest();
clipboardContent.putText(selectedText);
Clipboard.setContent(clipboardContent);
}
}
public void paste() {
if (document == null) {
setDocument(new Document());
}
Manifest clipboardContent = Clipboard.getContent();
if (clipboardContent != null && clipboardContent.containsText()) {
// Paste the string representation of the content
String text = null;
try {
text = clipboardContent.getText();
} catch (IOException exception) {
// No-op
}
if (text != null && text.length() > 0) {
// Remove any existing selection
if (selectionLength > 0) {
// TODO Make this part of the undoable action (for all such
// actions)
delete(true);
}
// Insert the clipboard contents
Document documentLocal;
int n;
try {
PlainTextSerializer serializer = new PlainTextSerializer();
StringReader reader = new StringReader(text);
serializer.setExpandTabs(this.expandTabs);
serializer.setTabWidth(((TextPane.Skin) getSkin()).getTabWidth());
documentLocal = serializer.readObject(reader);
n = documentLocal.getCharacterCount();
bulkOperation = true;
int start = selectionStart;
this.document.insertRange(documentLocal, start);
bulkOperation = false;
textPaneCharacterListeners.charactersInserted(this, start, n);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setSelection(selectionStart + n, 0);
}
}
}
public void undo() {
int n = editHistory.getLength();
if (n > 0) {
undoingHistory = true;
Edit edit = editHistory.remove(n - 1, 1).get(0);
edit.undo();
undoingHistory = false;
}
}
private void addHistoryItem(Edit edit) {
editHistory.add(edit);
if (editHistory.getLength() > MAXIMUM_EDIT_HISTORY_LENGTH) {
editHistory.remove(0, 1);
}
}
public void redo() {
// TODO
}
/**
* Add the text from the given element (and its children) to the given buffer,
* respecting the range of characters to be included.
*
* @param text The buffer we're building.
* @param element The current element in the document.
* @param includeSpan The range of text to be included (in document-relative
* coordinates).
*/
private void addToText(StringBuilder text, Element element, Span includeSpan) {
Span elementSpan = element.getDocumentSpan();
Span elementIntersection = elementSpan.intersect(includeSpan);
if (elementIntersection != null) {
for (Node node : element) {
if (node instanceof Element) {
addToText(text, (Element) node, includeSpan);
}
else {
Span nodeSpan = node.getDocumentSpan();
Span nodeIntersection = nodeSpan.intersect(includeSpan);
if (nodeIntersection != null) {
Span currentSpan = nodeIntersection.offset(-nodeSpan.start);
if (node instanceof TextNode) {
text.append(((TextNode) node).getCharacters(currentSpan));
} else if (node instanceof ComponentNode) {
text.append(((ComponentNode) node).getCharacters(currentSpan));
}
// TODO: anything more that could/should be handled?
// lists for instance???
}
}
}
if (element instanceof Paragraph && elementIntersection.end == elementSpan.end) {
// TODO: unclear if this is included in the character count for a paragraph or not
// or what that means for the intersection range above
text.append('\n');
}
}
}
/**
* Convenience method to get all the text from the current document into a
* single string.
*
* @return The complete text of the document as a string.
* @see #setText
*/
public String getText() {
int count;
Document doc = getDocument();
if (doc != null && (count = getCharacterCount()) != 0) {
StringBuilder text = new StringBuilder(count);
addToText(text, doc, new Span(0, count - 1));
return text.toString();
}
return null;
}
/**
* Convenience method to get a portion of the document text into a single string.
*
* @param beginIndex The 0-based offset where to start retrieving text.
* @param endIndex The ending offset + 1 of the text to retrieve.
* @return The specified portion of the document text if there is any, or
* {@code null} if there is no document.
*/
public String getText(int beginIndex, int endIndex) {
if (beginIndex > endIndex) {
throw new IllegalArgumentException("Beginning index " + beginIndex +
" is greater than ending index " + endIndex + ".");
}
if (beginIndex < 0 || endIndex > getCharacterCount()) {
throw new IndexOutOfBoundsException("Beginning index = " + beginIndex +
", ending index = " + endIndex + ", document.characterCount = " +
getCharacterCount() + ".");
}
int count = endIndex - beginIndex;
if (count == 0) {
return "";
}
Document doc = getDocument();
if (doc != null) {
StringBuilder text = new StringBuilder(count);
addToText(text, doc, new Span(beginIndex, endIndex - 1));
return text.toString();
}
return null;
}
/**
* Convenience method to create a text-only document consisting of one
* paragraph per line of the given text.
*
* @param text The new complete text for the document.
*/
public void setText(String text) {
Utils.checkNull(text, "text");
try {
setText(new StringReader(text));
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
public void setText(URL textURL) throws IOException {
Utils.checkNull(textURL, "text URL");
try (InputStream inputStream = textURL.openStream()) {
setText(new InputStreamReader(inputStream));
}
}
public void setText(Reader textReader) throws IOException {
Utils.checkNull(textReader, "Reader");
int tabPosition = 0;
int tabWidth = ((TextPane.Skin) getSkin()).getTabWidth();
Document doc = new Document();
StringBuilder text = new StringBuilder();
int c = textReader.read();
while (c != -1) {
// Deal with the various forms of line endings: CR only, LF only or CR,LF
if (c == '\r') {
int c2 = textReader.read();
if (c2 == -1) {
break;
} else if (c2 == '\n') {
// Only add the \n (the paragraph separator)
c = c2;
} else {
// Change the paragraph separator to \n instead
// but push back the last character read
Paragraph paragraph = new Paragraph(text.toString());
doc.add(paragraph);
text.setLength(0);
tabPosition = 0;
c = c2;
continue;
}
}
if (c == '\n') {
Paragraph paragraph = new Paragraph(text.toString());
doc.add(paragraph);
text.setLength(0);
tabPosition = 0;
} else if (c == '\t') {
if (expandTabs) {
int spaces = tabWidth - (tabPosition % tabWidth);
for (int i = 0; i < spaces; i++) {
text.append(' ');
}
tabPosition += spaces;
} else {
text.append('\t');
}
} else {
text.append((char) c);
tabPosition++;
}
c = textReader.read();
}
if (text.length() != 0) {
Paragraph paragraph = new Paragraph(text.toString());
doc.add(paragraph);
}
setDocument(doc);
}
/**
* @return The starting index of the selection.
*/
public int getSelectionStart() {
return selectionStart;
}
/**
* @return The length of the selection; may be <tt>0</tt>.
*/
public int getSelectionLength() {
return selectionLength;
}
/**
* Returns a span representing the current selection.
*
* @return A span containing the current selection. Both start and end
* points are inclusive. Returns <tt>null</tt> if the selection is empty.
*/
public Span getSelection() {
return (selectionLength == 0) ? null : new Span(selectionStart, selectionStart
+ selectionLength - 1);
}
/**
* Sets the selection. The sum of the selection start and length must be
* less than the length of the text input's content.
*
* @param selectionStart The starting index of the selection.
* @param selectionLength The length of the selection.
*/
public void setSelection(int selectionStart, int selectionLength) {
checkDocumentExists();
if (selectionLength < 0) {
throw new IllegalArgumentException("selectionLength is negative, selectionLength="
+ selectionLength);
}
indexBoundsCheck("selectionStart", selectionStart, 0, document.getCharacterCount() - 1);
if (selectionStart + selectionLength > document.getCharacterCount()) {
throw new IndexOutOfBoundsException("selectionStart=" + selectionStart
+ ", selectionLength=" + selectionLength + ", document.characterCount="
+ document.getCharacterCount());
}
int previousSelectionStart = this.selectionStart;
int previousSelectionLength = this.selectionLength;
if (previousSelectionStart != selectionStart || previousSelectionLength != selectionLength) {
this.selectionStart = selectionStart;
this.selectionLength = selectionLength;
textPaneSelectionListeners.selectionChanged(this, previousSelectionStart,
previousSelectionLength);
}
}
/**
* Sets the selection.
*
* @param selection The new span describing the selection.
* @see #setSelection(int, int)
*/
public final void setSelection(Span selection) {
Utils.checkNull(selection, "selection");
setSelection(Math.min(selection.start, selection.end), (int) selection.getLength());
}
/**
* Selects all text.
*/
public void selectAll() {
if (document == null) {
throw new IllegalStateException("document is null.");
}
setSelection(0, document.getCharacterCount());
}
/**
* Clears the selection.
*/
public void clearSelection() {
setSelection(0, 0);
}
/**
* Returns the currently selected text.
*
* @return A new string containing a copy of the text in the selected range,
* or <tt>null</tt> if nothing is selected.
*/
public String getSelectedText() {
String selectedText = null;
if (selectionLength > 0) {
Document selection = (Document) document.getRange(selectionStart, selectionLength);
try {
PlainTextSerializer serializer = new PlainTextSerializer();
StringWriter writer = new StringWriter();
serializer.writeObject(selection, writer);
selectedText = writer.toString();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
return selectedText;
}
/**
* @return The text pane's editable flag.
*/
public boolean isEditable() {
return editable;
}
/**
* Sets the text pane's editable flag.
*
* @param editable Whether or not the text should be editable now.
*/
public void setEditable(boolean editable) {
if (this.editable != editable) {
if (!editable) {
if (isFocused()) {
clearFocus();
}
}
this.editable = editable;
textPaneListeners.editableChanged(this);
}
}
public boolean getExpandTabs() {
return expandTabs;
}
/**
* Sets whether tab characters (<code>\t</code>) are expanded to an
* appropriate number of spaces during {@link #setText} and
* {@link #paste} operations. Note: doing this for keyboard input
* is handled in the skin.
*
* @param expandTabs <code>true</code> to replace tab characters with space
* characters (depending on the setting of the
* {@link TextPane.Skin#getTabWidth} value) or <code>false</code> to leave
* tabs alone. Note: this only affects tabs encountered during program
* operations; tabs entered via the keyboard by the user are always
* expanded, regardless of this setting.
*/
public void setExpandTabs(boolean expandTabs) {
this.expandTabs = expandTabs;
}
public int getInsertionPoint(int x, int y) {
TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
return textPaneSkin.getInsertionPoint(x, y);
}
public int getNextInsertionPoint(int x, int from, ScrollDirection direction) {
TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
return textPaneSkin.getNextInsertionPoint(x, from, direction);
}
public int getRowAt(int offset) {
TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
return textPaneSkin.getRowAt(offset);
}
public int getRowCount() {
TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
return textPaneSkin.getRowCount();
}
public Bounds getCharacterBounds(int offset) {
// We need to validate in case we get called from user-code after
// a user-code initiated modification, but before another layout has
// run.
validate();
TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
return textPaneSkin.getCharacterBounds(offset);
}
public ListenerList<TextPaneListener> getTextPaneListeners() {
return textPaneListeners;
}
public ListenerList<TextPaneCharacterListener> getTextPaneCharacterListeners() {
return textPaneCharacterListeners;
}
public ListenerList<TextPaneSelectionListener> getTextPaneSelectionListeners() {
return textPaneSelectionListeners;
}
}