blob: bf05aadb173de020d6c4eceeacf5f62870d74c41 [file] [log] [blame]
package org.eclipse.swt.custom;
/*
* (c) Copyright IBM Corp. 2001, 2002.
* All Rights Reserved
*/
import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.internal.*;
/**
* An instance of class <code>WrappedContent</code> is used by
* StyledText to display wrapped lines. Lines are wrapped at word
* breaks which are marked by a space character. Trailing space
* behind words is kept on the current line.
* If the last remaining word on a line can not be fully displayed
* the line is wrapped character by character.
* WrappedContent wraps a StyledTextContent which provides the line
* data. The start offset and length of wrapped lines is calculated
* and updated based on recalculation requests and text changes.
* <p>
* All public methods in this class implement the
* <code>StyledTextContent</code> interface. Package visible
* methods are internal API for use by <code>StyledText</code>.
* </p>
*/
class WrappedContent implements StyledTextContent {
final static int LINE_OFFSET = 0; // index of line offset in visualLines array
final static int LINE_LENGTH = 1; // index of line lenght in visualLines array
final static int WRAP_LINE_LENGTH = 0;
final static int WRAP_LINE_WIDTH = 1;
StyledText parent;
StyledTextContent logicalContent;
int[][] visualLines; // start and length of each visual line
int visualLineCount = 0;
/**
* Create a new instance.
*
* @param styledText StyledText widget that displays the lines
* wrapped by the new instance.
* @param logicalContent StyledTextContent that provides the line
* data.
*/
WrappedContent(StyledText styledText, StyledTextContent logicalContent) {
parent = styledText;
this.logicalContent = logicalContent;
}
/**
* @see StyledTextContent#addTextChangeListener(TextChangeListener)
*/
public void addTextChangeListener(TextChangeListener listener) {
logicalContent.addTextChangeListener(listener);
}
/**
* Grow the lines array to at least the specified size.
* <p>
*
* @param numLines number of elements that the array should have
* at a minimum
*/
private void ensureSize(int numLines) {
int size = visualLines.length;
if (size >= numLines) {
return;
}
int[][] newLines = new int[Math.max(size * 2, numLines)][2];
System.arraycopy(visualLines, 0, newLines, 0, size);
visualLines = newLines;
resetVisualLines(size, visualLines.length - size);
}
/**
* @see StyledTextContent#getCharCount()
*/
public int getCharCount() {
return logicalContent.getCharCount();
}
/**
* @return the visual (wrapped) line at the specified index
* @see StyledTextContent#getLine(int)
*/
public String getLine(int lineIndex) {
String line;
// redirect call to logical content if there are no wrapped lines
if (visualLineCount == 0) {
line = logicalContent.getLine(lineIndex);
}
else {
line = logicalContent.getTextRange(visualLines[lineIndex][LINE_OFFSET], visualLines[lineIndex][LINE_LENGTH]);
}
return line;
}
/**
* Returns the visual (wrapped) line at given offset.
* <p>
* The offset is ambiguous if it identifies the end of a visual line and
* there is another visual line below. In this case the end of the visual
* line has the same offset as the beginning of the next visual line
* since the visual line break is not represented by any character in the
* logical line.
* In this ambiguous case the offset is assumed to represent the end of a
* visual line and the index of the first visual line is returned.
* </p>
*
* @param offset offset of the desired line.
* @return the index of the visual (wrapped) line at the specified offset
* @see StyledTextContent#getLineAtOffset(int)
*/
public int getLineAtOffset(int offset) {
int lastLine = visualLineCount - 1;
int lastChar;
// redirect call to logical content if there are no wrapped lines
if (visualLineCount == 0) {
return logicalContent.getLineAtOffset(offset);
}
// can't use getCharCount to get the number of characters since this
// method is called in textChanged, when the logicalContent used by
// getCharCount has already changed. at that point the visual lines
// have not been updated yet and we thus need to use the old character
// count which is only available in the visual content.
lastChar = visualLines[lastLine][LINE_OFFSET] + visualLines[lastLine][LINE_LENGTH];
if (offset < 0 || (offset > 0 && offset > lastChar)) {
SWT.error(SWT.ERROR_INVALID_ARGUMENT);
}
// if last line and the line is not empty you can ask for
// a position that doesn't exist (the one to the right of the
// last character) - for inserting
if (offset == lastChar) {
return lastLine;
}
int high = visualLineCount;
int low = -1;
int index = visualLineCount;
while (high - low > 1) {
index = (high + low) / 2;
int lineStart = visualLines[index][LINE_OFFSET];
if (offset >= lineStart) {
int lineEnd = lineStart + visualLines[index][LINE_LENGTH];
low = index;
if (offset <= lineEnd) {
break;
}
}
else {
high = index;
}
}
if (low > 0 && offset == visualLines[low - 1][LINE_OFFSET] + visualLines[low - 1][LINE_LENGTH]) {
// end of a visual line/beginning of next visual line is ambiguous
// (they have the same offset). always return the first visual line
low--;
}
return low;
}
/**
* @return the number of visual (wrapped) lines
* @see StyledTextContent#getLineCount()
*/
public int getLineCount() {
int lineCount = visualLineCount;
// redirect call to logical content if there are no wrapped lines
if (visualLineCount == 0) {
lineCount = logicalContent.getLineCount();
}
return lineCount;
}
/**
* @see StyledTextContent#getLineDelimiter()
*/
public String getLineDelimiter() {
return logicalContent.getLineDelimiter();
}
/**
* @return the start offset of the visual (wrapped) line at the given
* index
* @see StyledTextContent#getOffsetAtLine(int)
*/
public int getOffsetAtLine(int lineIndex) {
int offset;
// redirect call to logical content if there are no wrapped lines
if (visualLineCount == 0) {
offset = logicalContent.getOffsetAtLine(lineIndex);
}
else {
offset = visualLines[lineIndex][LINE_OFFSET];
}
return offset;
}
/**
* @see StyledTextContent#getTextRange(int, int)
*/
public String getTextRange(int start, int length) {
return logicalContent.getTextRange(start, length);
}
/**
* Returns the offset of the character after the word at the specified
* offset.
* <p>
* Words are separated by spaces. Trailing spaces are considered part
* of the word.
* </p>
*
* @param line logical line the word is in
* @param startOffset start offset of the line, relative to the start
* of the logical line.
* @param offset offset of the word to return the end of, relative to
* the start of the visual line.
* @return the offset of the character after the word at the specified
* offset.
*/
private int getWordEnd(String line, int startOffset, int offset) {
int lineLength = line.length();
offset += startOffset;
if (offset >= lineLength) {
return offset - startOffset;
}
// skip over leading whitespace
do {
offset++;
} while (offset < lineLength && Compatibility.isSpaceChar(line.charAt(offset)));
while (offset < lineLength && Compatibility.isSpaceChar(line.charAt(offset)) == false) {
offset++;
}
// skip over trailing whitespace
while (offset < lineLength && Compatibility.isSpaceChar(line.charAt(offset))) {
offset++;
}
return offset - startOffset;
}
/**
* Returns the start offset of the word at the specified offset.
* There are two classes of words formed by a sequence of characters:
* <p>
* Words are separated by spaces. Trailing spaces are considered part
* of the word.
* </p>
*
* @param line logical line the word is in
* @param startOffset start offset of the line, relative to the start
* of the logical line.
* @param offset offset of the word to return the start of, relative to
* the start of the visual line.
* @return the start offset of the word at the specified offset.
*/
private int getWordStart(String line, int startOffset, int offset) {
offset += startOffset;
// skip over trailing whitespace
do {
offset--;
} while (offset > startOffset && Compatibility.isSpaceChar(line.charAt(offset)));
while (offset > startOffset && Compatibility.isSpaceChar(line.charAt(offset - 1)) == false) {
offset--;
}
return offset - startOffset;
}
/**
* @see StyledTextContent#removeTextChangeListener(TextChangeListener)
*/
public void removeTextChangeListener(TextChangeListener listener) {
logicalContent.removeTextChangeListener(listener);
}
/**
* Reset the visual (wrapped) lines in the specified range.
* If the range specifies partial logical lines (e.g., startLine is
* the second of two visual lines) it is extended to reset all visual
* lines of a logical line.
* Following the reset the logical lines in the reset visual range are
* rewrapped.
* <p>
*
* @param startLine index of the first visual line
* @param lineCount number of visual lines
*/
void reset(int startLine, int lineCount) {
if (lineCount <= 0 || visualLineCount == 0) {
return;
}
reset(startLine, lineCount, true);
}
/**
* Reset the visual (wrapped) lines in the specified range.
* If the range specifies partial logical lines (e.g., startLine is
* the second of two visual lines) it is extended to reset all visual
* lines of a logical line.
* <p>
*
* @param startLine index of the first visual line
* @param lineCount number of visual lines
* @param wrap true=rewrap the logical lines in the reset visual range
* false=don't rewrap lines. Visual lines will be left in an inconsistent
* state since there will be a range of unwrapped and unknown lines.
* @return the first visual line that was reset
*/
private int reset(int startLine, int lineCount, boolean wrap) {
if (lineCount <= 0) {
return startLine;
}
// make sure that all visual lines of the first logical line are
// being reset. visualFirstLine is the first visual line of the
// first logical line that has at least one visual line reset.
int visualFirstLineOffset = getOffsetAtLine(startLine);
int logicalFirstLine = logicalContent.getLineAtOffset(visualFirstLineOffset);
int logicalFirstLineOffset = logicalContent.getOffsetAtLine(logicalFirstLine);
int visualFirstLine = getLineAtOffset(logicalFirstLineOffset);
lineCount += startLine - visualFirstLine;
startLine = visualFirstLine;
// make sure that all visual lines of the last logical line are
// being reset.
int lastLine = startLine + lineCount - 1;
int lastLineEnd = visualLines[lastLine][LINE_OFFSET] + visualLines[lastLine][LINE_LENGTH];
int logicalEndLine = 0;
while (lastLine < visualLineCount - 1 && lastLineEnd == visualLines[lastLine + 1][LINE_OFFSET]) {
lastLine++;
lastLineEnd = visualLines[lastLine][LINE_OFFSET] + visualLines[lastLine][LINE_LENGTH];
}
if (wrap) {
if (lastLine == visualLineCount - 1) {
logicalEndLine = logicalContent.getLineCount();
}
else {
logicalEndLine = logicalContent.getLineAtOffset(visualLines[lastLine + 1][LINE_OFFSET]);
}
}
lineCount = lastLine - startLine + 1;
resetVisualLines(startLine, lineCount);
visualLineCount -= lineCount;
if (wrap) {
// always recalculate line wrap immediately after a reset
// because the content always needs to be in a usable state.
// i.e., there must not be any reset but unwrapped lines
wrapLineRange(logicalFirstLine, logicalEndLine, startLine);
}
return startLine;
}
/**
* Reset the visual (wrapped) lines in the specified range.
* <p>
*
* @param startLine index of the first visual line
* @param lineCount number of visual lines
*/
private void resetVisualLines(int startLine, int lineCount) {
int endLine = startLine + lineCount;
for (int i = startLine; i < endLine; i++) {
visualLines[i] = new int[] {-1, -1};
}
}
/**
* @see StyledTextContent#replaceTextRange(int, int, String)
*/
public void replaceTextRange(int start, int replaceLength, String text) {
logicalContent.replaceTextRange(start, replaceLength, text);
}
/**
* @see StyledTextContent#setText(String)
*/
public void setText(String text) {
logicalContent.setText(text);
}
/**
* Set the line wrap data for the specified visual (wrapped) line.
* <p>
*
* @param visualLineIndex index of the visual line
* @param visualLineOffset start offset of the visual line, relative
* to the start of the document
* @param visualLineLength length of the visual line
*/
private void setVisualLine(int visualLineIndex, int visualLineOffset, int visualLineLength) {
ensureSize(visualLineCount + 1);
// is the space for the visual line already taken? can happen if
// there are more visual lines for a given logical line than before
if (visualLines[visualLineIndex][LINE_OFFSET] != -1) {
System.arraycopy(visualLines, visualLineIndex, visualLines, visualLineIndex + 1, visualLineCount - visualLineIndex);
visualLines[visualLineIndex] = new int[2];
}
visualLines[visualLineIndex][LINE_OFFSET] = visualLineOffset;
visualLines[visualLineIndex][LINE_LENGTH] = visualLineLength;
visualLineCount++;
}
/**
* Recalculates the line wrap for the lines affected by the
* text change.
* <p>
*
* @param startOffset the start offset of the text change
* @param newLineCount the number of inserted lines
* @param replaceLineCount the number of deleted lines
* @param newCharCount the number of new characters
* @param replaceCharCount the number of deleted characters
*/
void textChanged(int startOffset, int newLineCount, int replaceLineCount, int newCharCount, int replaceCharCount) {
// do nothing if there are no wrapped lines
if (visualLineCount == 0) {
return;
}
int logicalStartLine = logicalContent.getLineAtOffset(startOffset);
int visualStartLine = getLineAtOffset(startOffset);
int visualReplaceLastLine = visualLineCount - 1;
int textChangeDelta = newCharCount - replaceCharCount;
if (replaceLineCount > 0) {
visualReplaceLastLine = getLineAtOffset(startOffset + replaceCharCount);
// at the start of a visual line/end of the previous visual line?
if (visualReplaceLastLine == 0 ||
visualLines[visualReplaceLastLine][LINE_OFFSET] == visualLines[visualReplaceLastLine - 1][LINE_OFFSET] + visualLines[visualReplaceLastLine - 1][LINE_LENGTH]) {
visualReplaceLastLine++;
}
visualStartLine = reset(visualStartLine, visualReplaceLastLine - visualStartLine + 1, false);
}
else {
visualStartLine = reset(visualStartLine, 1, false);
}
visualReplaceLastLine = wrapLineRange(logicalStartLine, logicalStartLine + 1 + newLineCount, visualStartLine);
for (int i = visualReplaceLastLine; i < visualLineCount; i++) {
visualLines[i][LINE_OFFSET] += textChangeDelta;
}
}
/**
* Measure the width of a segment in the specified logical line.
* <p>
*
* @param line the logical line
* @param logicalLineOffset start offset of the logical line, relative
* to the start of the document
* @param visualLineOffset offset to start measuring at/start offset
* of the visual line
* @param visualLineLength length of the segment to measure/the visual
* line
* @param styles StyleRanges to use during measuring
* @param startX x position of the visual line relative to the start
* of the logical line
* @param gc GC to use for measuring
* @param fontData FontData currently set in gc. Used to reduce calls
* to gc.getFont().getFontData()
*/
private int textWidth(String line, int logicalLineOffset, int visualLineOffset, int visualLineLength, StyleRange[] styles, int startX, GC gc, FontData fontData) {
int width;
if (styles != null) {
// while wrapping a line, the logcial line styles may contain
// style ranges that don't apply (i.e., completely on the previous/next
// visual line). Therefore we need to filter the logical lines.
styles = parent.getVisualLineStyleData(styles, logicalLineOffset + visualLineOffset, visualLineLength);
}
if (parent.isBidi()) {
String wrappedLine = line.substring(visualLineOffset, visualLineOffset + visualLineLength);
StyledTextBidi bidi = parent.getStyledTextBidi(wrappedLine, logicalLineOffset + visualLineOffset, gc, styles);
width = bidi.getTextWidth();
}
else {
width = parent.textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength, styles, startX, gc, fontData);
}
return width;
}
/**
* Wrap the given logical line at the specified offset.
* Called repeatedly until the entire logical lines has been split into
* visual (wrapped) lines.
* <p>
*
* @param line the logical line
* @param logicalLineOffset offset of the logical line, relative to the
* beginning of the content
* @param visualLineOffset start offset of the new visual line, relative
* to the start of the logical line.
* @param startX x position of visualLineOffset, relative to the beginning
* of the logical line
* @param width width to wrap the line to
* @param numChars average number of characters that fit into width
* @param styles StyleRanges to use for measuring the wrapped line
* @param gc GC to use for measuring
* @param fontData FontData currently set in gc. Used to reduce calls to
* gc.getFont().getFontData()
* @return int[0]=length of the new visual line, int[1]=width in pixels
* of the new visual line
*/
private int[] wrapLine(String line, int logicalLineOffset, int visualLineOffset, int startX, int width, int numChars, StyleRange[] styles, GC gc, FontData fontData) {
int lineLength = line.length();
int lineWidth = 0;
int visualLineLength;
numChars = Math.min(numChars, lineLength - visualLineOffset);
visualLineLength = getWordStart(line, visualLineOffset, numChars);
// find a word that is within the client area. make sure at least one
// character is on each line so that line wrap algorithm terminates.
if (visualLineLength > 0) {
lineWidth = textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength, styles, startX, gc, fontData);
if (lineWidth >= width) {
while (visualLineLength > 1 && lineWidth >= width) {
visualLineLength = getWordStart(line, visualLineOffset, visualLineLength);
lineWidth = textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength, styles, startX, gc, fontData);
}
}
else
if (lineWidth < width) {
while (visualLineOffset + visualLineLength < lineLength) {
int newLineLength = getWordEnd(line, visualLineOffset, visualLineLength);
int newLineWidth = textWidth(line, logicalLineOffset, visualLineOffset, newLineLength, styles, startX, gc, fontData);
// would next word be beyond client area?
if (newLineWidth >= width) {
break;
}
else {
visualLineLength = newLineLength;
lineWidth = newLineWidth;
}
}
}
}
if (visualLineLength <= 0) {
// no complete word fits on the line. either first word was not within
// estimated number of characters or it was beyond the line width even
// though it was within numChars.
visualLineLength = numChars;
lineWidth = textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength, styles, startX, gc, fontData);
if (lineWidth >= width) {
while (visualLineLength > 1 && lineWidth >= width) {
visualLineLength--;
lineWidth = textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength, styles, startX, gc, fontData);
}
}
else
if (lineWidth < width) {
while (visualLineOffset + visualLineLength < lineLength) {
int newLineWidth = textWidth(line, logicalLineOffset, visualLineOffset, visualLineLength + 1, styles, startX, gc, fontData);
if (newLineWidth >= width) {
break;
}
else {
visualLineLength++;
lineWidth = newLineWidth;
}
}
}
}
return new int[] {visualLineLength, lineWidth};
}
/**
* Wrap the logical lines in the given range at the current client
* area width of the StyledText widget
* <p>
*
* @param startLine first logical line to wrap
* @param endLine line after last logical line
* @param visualLineIndex visual (wrapped) line index that startLine
* corresponds to.
* @return index of the line following the last wrapped line
*/
private int wrapLineRange(int startLine, int endLine, int visualLineIndex) {
int emptyLineCount = 0;
visualLineIndex = wrapLineRange(startLine, endLine, visualLineIndex, parent.getClientArea().width);
// is there space left for more visual lines? can happen if there are fewer
// visual lines for a given logical line than before
for (int i = visualLineIndex; i < visualLines.length; i++, emptyLineCount++) {
if (visualLines[i][LINE_OFFSET] != -1) {
break;
}
}
if (emptyLineCount > 0) {
int copyLineCount = visualLineCount - visualLineIndex;
System.arraycopy(visualLines, visualLineIndex + emptyLineCount, visualLines, visualLineIndex, copyLineCount);
resetVisualLines(visualLineIndex + copyLineCount, emptyLineCount);
}
return visualLineIndex;
}
/**
* Wrap the lines in the given range. Skip lines that have already
* been wrapped.
* <p>
*
* @param startLine first logical line to wrap
* @param endLine line after last logical line
* @param visualLineIndex visual (wrapped) line index that startLine
* corresponds to.
* @param width line width to wrap at
* @return index of last wrapped line
*/
private int wrapLineRange(int startLine, int endLine, int visualLineIndex, int width) {
GC gc = new GC(parent);
FontData fontData = gc.getFont().getFontData()[0];
int numChars = Math.max(1, width / gc.getFontMetrics().getAverageCharWidth());
// if there are no wrapped lines and the width is 0 the widget has
// not been made visible/sized yet. don't wrap until the widget size
// is known.
if (visualLineCount == 0 && width == 0) {
return visualLineIndex;
}
for (int i = startLine; i < endLine; i++) {
String line = logicalContent.getLine(i);
int lineOffset = logicalContent.getOffsetAtLine(i);
int lineLength = line.length();
if (lineLength == 0) {
setVisualLine(visualLineIndex, lineOffset, 0);
visualLineIndex++;
continue;
}
StyleRange[] styles = null;
StyledTextEvent event = parent.getLineStyleData(lineOffset, line);
int startOffset = 0;
int startX = 0;
if (event != null) {
styles = parent.filterLineStyles(event.styles);
}
while (startOffset < lineLength) {
int[] result = wrapLine(line, lineOffset, startOffset, startX, width, numChars, styles, gc, fontData);
setVisualLine(visualLineIndex, lineOffset + startOffset, result[WRAP_LINE_LENGTH]);
startOffset += result[WRAP_LINE_LENGTH];
startX += result[WRAP_LINE_WIDTH];
visualLineIndex++;
}
}
gc.dispose();
return visualLineIndex;
}
/**
* Wrap all logical lines at the current client area width of the
* StyledText widget
*/
void wrapLines() {
wrapLines(parent.getClientArea().width);
}
/**
* Wrap all logical lines at the given width.
* <p>
*
* @param width width to wrap lines at
*/
void wrapLines(int width) {
int lineCount = logicalContent.getLineCount();
visualLineCount = 0;
visualLines = new int[lineCount][2];
resetVisualLines(0, visualLines.length);
wrapLineRange(0, lineCount, 0, width);
}
}