package org.eclipse.swt.custom; | |
/* | |
* (c) Copyright IBM Corp. 2000, 2001. | |
* All Rights Reserved | |
*/ | |
import org.eclipse.swt.events.*; | |
import org.eclipse.swt.graphics.*; | |
import java.util.*; | |
class DefaultLineStyler implements LineStyleListener, LineBackgroundListener { | |
StyledTextContent content; | |
StyleRange styles[] = new StyleRange[0]; | |
int styleCount = 0; // the number of styles | |
int styleExpandExp = 1; // the expansion exponent, used to increase the styles array exponentially | |
int lineExpandExp = 1; // the expansion exponent, used to increase the lines array exponentially | |
int lineCount = 0; | |
Color lineBackgrounds[]; | |
/** | |
* Creates a new default line styler. | |
* <p> | |
* | |
* @param content the text to which the styles apply | |
*/ | |
public DefaultLineStyler(StyledTextContent content) { | |
this.content = content; | |
lineCount = content.getLineCount(); | |
lineBackgrounds = new Color[lineCount]; | |
} | |
/** | |
* Inserts a style at the given location. | |
* <p> | |
* | |
* @param style the new style | |
* @param index the index at which to insert the style (the new style | |
* will reside at this index) | |
* | |
*/ | |
void insertStyle(StyleRange style, int index) { | |
int size = styles.length; | |
if (styleCount == size) { | |
// expand the styles array by powers of 2 | |
StyleRange[] newStyles = new StyleRange[size+(int)Math.pow(2, styleExpandExp)]; | |
System.arraycopy(styles, 0, newStyles, 0, size); | |
styles = newStyles; | |
styleExpandExp++; | |
} | |
// shift the styles down to make room for the new style | |
for (int i=styleCount-1; i>=index; i--) { | |
styles[i+1]=styles[i]; | |
} | |
styles[index] = style; | |
styleCount++; | |
} | |
/** | |
* Inserts a style, merging it with adjacent styles if possible. | |
* <p> | |
* | |
* @param style the new style | |
* @param index the index at which to insert the style (the new style | |
* will reside at this index) | |
* @return true if the style was inserted, false if the style was merged with an adjacent | |
* style | |
*/ | |
boolean insertMergeStyle(StyleRange style, int index) { | |
if (mergeStyleBefore(style, index)) return false; | |
if (mergeStyleAfter(style, index)) return false; | |
insertStyle(style, index); | |
return true; | |
} | |
/** | |
* Merges the style with the style before it if possible. | |
* <p> | |
* | |
* @param style the new style | |
* @param index the index at which to attempt the merge. | |
* @return true if the style was merged, false otherwise | |
*/ | |
boolean mergeStyleBefore(StyleRange style, int index) { | |
// see if the style is similar to the style before it and merge the | |
// styles if possible | |
if (index > 0) { | |
StyleRange previous = styles[index-1]; | |
if (style.similarTo(previous)) { | |
// the start of style needs to be in the range of the previous style | |
// and the end of style needs to be < the start of the next style | |
int previousEnd = previous.start + previous.length; | |
if ((style.start <= previousEnd) && (style.start >= previous.start)) { | |
int styleEnd = style.start + style.length; | |
if ((index == styleCount) || (styleEnd <= styles[index].start)) { | |
previous.length = style.start + style.length - previous.start; | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Merges the style with the style after it if possible. | |
* <p> | |
* | |
* @param style the new style | |
* @param index the index at which to attempt the merge. | |
* @return true if the style was merged, false otherwise | |
*/ | |
boolean mergeStyleAfter(StyleRange style, int index) { | |
// see if the style is similar to the style that will be after it and | |
// merge the styles if possible | |
if (index < styleCount) { | |
StyleRange next = styles[index]; | |
if (style.similarTo(next)) { | |
// the end of style needs to be in the range of the next style and | |
// the start of style needs to be > the end of the previous style | |
int styleEnd = style.start + style.length; | |
int nextEnd = next.start + next.length; | |
if ((styleEnd <= nextEnd) && (styleEnd >= next.start)) { | |
if ((index == 0) || (style.start >= styles[index-1].start + styles[index-1].length)) { | |
next.length = next.start + next.length - style.start; | |
next.start = style.start; | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Removes style information that is defined for the range of text in <code>clearStyle</code>. | |
* <p> | |
* | |
* @param clearStyle the style information to use for clearing | |
*/ | |
void clearStyle(StyleRange clearStyle) { | |
Point pt = getOverlappingStyles(clearStyle.start, clearStyle.length); | |
int clearStyleEnd = clearStyle.start + clearStyle.length - 1; | |
// no overlapped styles exist | |
if ((pt == null) || (pt.y == 0)) return; | |
// the newStyle overlaps one or more of the existing styles | |
// pt.x is the index of the first overlapped style, pt.y is the number of overlapped | |
// styles | |
int count = 0; | |
for (int i=pt.x; count<pt.y; i++) { | |
StyleRange overlap = styles[i]; | |
int overlapEnd = overlap.start + overlap.length - 1; | |
if (overlap.start < clearStyle.start) { | |
if (overlapEnd <= clearStyleEnd) { | |
// the end of overlap needs to be cleared | |
overlap.length=clearStyle.start - overlap.start; | |
} else { | |
// middle of overlap needs to be cleared, this will | |
// cause overlap to be broken into two | |
StyleRange endStyle = (StyleRange)overlap.clone(); | |
endStyle.start = clearStyleEnd + 1; | |
endStyle.length = overlapEnd - clearStyleEnd; | |
overlap.length = clearStyle.start - overlap.start; | |
insertStyle(endStyle, i+1); | |
break; | |
} | |
} else { | |
if (overlapEnd <= clearStyleEnd) { | |
// entire overlap needs to be cleared | |
deleteStyle(i); | |
i--; | |
} else { | |
// beginning of overlap needs to be cleared | |
overlap.start=clearStyleEnd + 1; | |
overlap.length=overlapEnd - overlap.start + 1; | |
break; | |
} | |
} | |
count++; | |
} | |
} | |
/** | |
* Increases the <code>linebackgrounds</code> array to accomodate new line background | |
* information. | |
* <p> | |
* | |
* @param numLines the number to increase the array by | |
*/ | |
void expandLinesBy(int numLines) { | |
int size = lineBackgrounds.length; | |
if (size - lineCount >= numLines) { | |
return; | |
} | |
Color[] newLines = new Color[size+Math.max((int)Math.pow(2, lineExpandExp), numLines)]; | |
System.arraycopy(lineBackgrounds, 0, newLines, 0, size); | |
lineBackgrounds = newLines; | |
lineExpandExp++; | |
} | |
/** | |
* Deletes the style at <code>index</code>. | |
* <p> | |
* | |
* @param index the index of the style to be deleted | |
*/ | |
void deleteStyle(int index) { | |
// move the styles up | |
for (int i=index+1; i<styleCount; i++) { | |
styles[i-1] = styles[i]; | |
} | |
styles[styleCount-1]=null; | |
styleCount--; | |
} | |
/** | |
* Returns the styles that are defined. | |
* <p> | |
* | |
* @return the copied array of styles | |
*/ | |
StyleRange [] getStyleRanges() { | |
StyleRange[] newStyles = new StyleRange[styleCount]; | |
System.arraycopy(styles, 0, newStyles, 0, styleCount); | |
return newStyles; | |
} | |
/** | |
* Handles the get line background color callback. | |
* <p> | |
* | |
* @param event.lineOffset line number (input) | |
* @param event.lineText line text (input) | |
* @param event.background line background color (output) | |
*/ | |
public void lineGetBackground(LineBackgroundEvent event) { | |
event.lineBackground = lineBackgrounds[content.getLineAtOffset(event.lineOffset)]; | |
} | |
/** | |
* Handles the get line style information callback. | |
* <p> | |
* | |
* @param event.lineOffset line number (input) | |
* @param event.lineText line text (input) | |
* @param event.styles array of StyleRanges, need to be in order (output) | |
*/ | |
public void lineGetStyle(LineStyleEvent event) { | |
int lineStart = event.lineOffset; | |
int lineEnd = lineStart + event.lineText.length(); | |
int high = searchForStyle(lineStart, lineEnd); | |
StyleRange style = null; | |
Vector lineStyles = new Vector(); | |
// index will represent a style that | |
// -- starts after the line (end processing) | |
// -- ends before the line (continue processing) | |
// -- starts before the line, ends in the line (add range) | |
// -- starts in the line, ends in the line (add range) | |
// -- starts in the line, ends after the line (add range) | |
// -- starts before the line, ends after the line (add range) | |
for (int index = high; index < styleCount; index++) { | |
style = styles[index]; | |
if (style.start > lineEnd) | |
// style starts after the line, end looping | |
break; | |
int styleEnd = style.start + style.length - 1; | |
if (styleEnd >= lineStart) lineStyles.add(style); | |
} | |
event.styles = new StyleRange[lineStyles.size()]; | |
lineStyles.copyInto(event.styles); | |
} | |
/** | |
* Searches for the first style in the <code>start</code> - <code>end</code> range. | |
* <p> | |
* | |
* @return the index of the first style that overlaps the input range | |
*/ | |
int searchForStyle(int start, int end) { | |
int high = styleCount; | |
int low = -1; | |
int index = high; | |
// find the index of the first style for the given range, use a binary search | |
while (high - low > 1) { | |
index = (high + low) / 2; | |
StyleRange style = styles[index]; | |
int styleEnd = style.start + style.length - 1; | |
if (start <= style.start || end <= styleEnd || (start > style.start && styleEnd >= start && styleEnd < end)) { | |
high = index; | |
} | |
else { | |
low = index; | |
} | |
} | |
return high; | |
} | |
/** | |
* Updates the line background colors to reflect a new color. Called by StyledText. | |
* <p> | |
* | |
* @param startLine index of the first line to color | |
* @param lineCount number of lines to color starting at startLine | |
* @param background the background color for the lines | |
*/ | |
void setLineBackground(int startLine, int count, Color background) { | |
for (int i=startLine; i<startLine + count; i++) { | |
lineBackgrounds[i]=background; | |
} | |
} | |
/** | |
* Update the styles to reflect the new style. <code>newStyle</code> will | |
* replace any old style for the range. When this method is called, the | |
* DefaultLineStyler may merge the new style with an existing style (if possible). | |
* Called by StyledText when a style is added. Called by StyledText. | |
* <p> | |
* | |
* @param newStyle the new style information. | |
*/ | |
void setStyleRange(StyleRange newStyle) { | |
if (newStyle == null) { | |
styles = new StyleRange[0]; | |
styleExpandExp = 1; | |
styleCount = 0; | |
return; | |
} | |
if (newStyle.length ==0) return; | |
if (newStyle.isUnstyled()) { | |
clearStyle(newStyle); | |
return; | |
} | |
Point pt = getOverlappingStyles(newStyle.start, newStyle.length); | |
int newStyleEnd = newStyle.start + newStyle.length - 1; | |
// no styles exist | |
if (pt == null) { | |
insertStyle(newStyle, 0); | |
return; | |
} | |
// newStyle does not overlap any other styles | |
if (pt.y == 0) { | |
insertMergeStyle(newStyle, pt.x); | |
return; | |
} | |
// the newStyle overlaps one or more of the existing styles | |
boolean added = false; // indicates whether or not the new style has been added | |
int count = 0; | |
// pt.x is the index of the first overlapped style, pt.y is the number of overlapped | |
// styles | |
for (int i=pt.x; count<pt.y; i++) { | |
StyleRange overlap = styles[i]; | |
int overlapEnd = overlap.start + overlap.length - 1; | |
if (overlap.start < newStyle.start) { | |
if (overlapEnd <= newStyleEnd) { | |
// the end of overlap needs to be replaced by newStyle | |
if (newStyle.similarTo(overlap)) { | |
// update overlap to accomodate the new style | |
overlap.length = newStyle.start + newStyle.length - overlap.start; | |
} else { | |
overlap.length=newStyle.start - overlap.start; | |
// see if newStyle can be merged with the style after overlap, if so, | |
// processing is done | |
if (mergeStyleAfter(newStyle, i+1)) break; | |
// otherwise, insert the newStyle, newStyle may still overlap other | |
// styles after it so continue processing | |
insertStyle(newStyle, i+1); | |
i++; | |
} | |
added = true; | |
} else { | |
// middle of overlap needs to be replaced by newStyle, this will | |
// cause overlap to be broken into two | |
if (newStyle.similarTo(overlap)) break; | |
StyleRange endStyle = (StyleRange)overlap.clone(); | |
endStyle.start = newStyleEnd + 1; | |
endStyle.length = overlapEnd - newStyleEnd; | |
overlap.length = newStyle.start - overlap.start; | |
insertStyle(newStyle, i+1); | |
i++; | |
insertStyle(endStyle, i+1); | |
// when newStyle overlaps the middle of a style, this implies that | |
// processing is done (no more overlapped styles) | |
break; | |
} | |
} else { | |
if (overlapEnd <= newStyleEnd) { | |
// overlap will be replaced by the newStyle, make sure newStyle | |
// hasn't already been added, if it has just delete overlap | |
if (!added) { | |
styles[i] = newStyle; | |
added = true; | |
} else { | |
deleteStyle(i); | |
i--; | |
} | |
} else { | |
// beginning of overlap needs to be replaced by newStyle | |
overlap.start=newStyleEnd + 1; | |
overlap.length=overlapEnd - overlap.start + 1; | |
if (!added) { | |
insertMergeStyle(newStyle, i); | |
} | |
// when newStyle overlaps only the beginning of a style, this implies | |
// that processing is done (no more overlapped styles) | |
break; | |
} | |
} | |
count++; | |
} | |
} | |
/** | |
* Sets the array of styles and discards old styles. Called by StyledText. | |
* <p> | |
* | |
* @param styles the new styles, must be in order and non-overlapping | |
*/ | |
void setStyleRanges(StyleRange[] styles) { | |
this.styles = new StyleRange[styles.length]; | |
System.arraycopy(styles, 0, this.styles, 0, styles.length); | |
styleCount = styles.length; | |
styleExpandExp = 1; | |
} | |
/** | |
* Updates the style ranges and line backgrounds to reflect a pending text | |
* change. | |
* Called by StyledText when a TextChangingEvent is received. | |
* <p> | |
* | |
* @param event the event with the text change information | |
*/ | |
public void textChanging(TextChangingEvent event) { | |
int startLine = content.getLineAtOffset(event.start); | |
int startLineOffset = content.getOffsetAtLine(startLine); | |
textChanging(event.start, -event.replaceCharCount); | |
textChanging(event.start, event.newCharCount); | |
if (event.replaceCharCount == content.getCharCount()) { | |
// all text is going to be replaced, clear line backgrounds | |
linesChanging(0, -lineCount); | |
linesChanging(0, content.getLineCount() - event.replaceLineCount + event.newLineCount); | |
return; | |
} | |
if (event.start != startLineOffset) { | |
startLine = startLine + 1; | |
} | |
linesChanging(startLine, -event.replaceLineCount); | |
linesChanging(startLine, event.newLineCount); | |
} | |
/* | |
* Updates the line backgrounds to reflect a pending text change. | |
* <p> | |
* | |
* @param start the starting line of the change that is about to take place | |
* @param delta the number of lines in the change, > 0 indicates lines inserted, | |
* < 0 indicates lines deleted | |
*/ | |
void linesChanging(int start, int delta) { | |
if (delta == 0) return; | |
boolean inserting = delta > 0; | |
if (inserting) { | |
// shift the lines down to make room for new lines | |
expandLinesBy(delta); | |
for (int i = lineCount-1; i >= start; i--) { | |
lineBackgrounds[i + delta]=lineBackgrounds[i]; | |
} | |
for (int i=start; i<start + delta; i++) { | |
lineBackgrounds[i]=null; | |
} | |
} else { | |
// shift up the lines | |
for (int i = start - delta; i < lineCount; i++) { | |
lineBackgrounds[i+delta]=lineBackgrounds[i]; | |
} | |
} | |
lineCount += delta; | |
} | |
/* | |
* Updates the style ranges to reflect a text change. | |
* <p> | |
* | |
* @param start the starting offset of the change that is about to | |
* take place | |
* @param delta the length of the change, > 0 indicates text inserted, | |
* < 0 indicates text deleted | |
*/ | |
void textChanging(int start, int delta) { | |
if (delta == 0) return; | |
StyleRange style; | |
// find the index of the first style for the given offset, use a binary search | |
// to find the index | |
int end; | |
boolean inserting = delta > 0; | |
if (inserting) { | |
end = (start + delta) - 1; | |
} else { | |
end = (start - delta) - 1; | |
} | |
int high = searchForStyle(start, end); | |
int index; | |
// update the styles that are in the affected range | |
for (index = high; index < styleCount; index++) { | |
style = styles[index]; | |
if (inserting) { | |
if (style.start >= start) break; | |
// in the insert case only one style range will be directly affected, | |
// it will need to be split into two and then the newStyle inserted | |
StyleRange beforeStyle = (StyleRange)style.clone(); | |
beforeStyle.length = start - style.start; | |
style.start = start; | |
style.length = style.length - beforeStyle.length; | |
if (beforeStyle.length != 0) insertStyle(beforeStyle, index); | |
index++; | |
break; | |
} else { | |
int styleEnd = style.start + style.length - 1; | |
if (style.start > end) break; | |
// in the delete case, any style that overlaps the change range will be | |
// affected | |
if (style.start < start) { | |
if (styleEnd <= end) { | |
// style starts before change range, ends in change range | |
style.length = start - style.start; | |
} else { | |
// style starts before change range, ends after change range | |
style.length = style.length + delta; | |
index++; | |
break; | |
} | |
} else { | |
if (styleEnd <= end) { | |
// style starts in change range, ends in change range | |
deleteStyle(index); | |
index--; | |
} else { | |
// style starts in change range, ends after change range | |
style.start = start; | |
style.length = styleEnd - end; | |
index++; | |
break; | |
} | |
} | |
} | |
} | |
// change the offsets of the styles after the affected styles | |
for (int i = index ; i < styleCount; i++) { | |
style = styles[i]; | |
style.start = style.start + delta; | |
} | |
} | |
/** | |
* Returns the indexes of the styles that overlap the given range. Styles that partially | |
* or fully overlap the range will be returned. | |
* <p> | |
* | |
* @return Point where x is the index of the starting overlap style, y is the number of | |
* styles that overlap the range | |
*/ | |
Point getOverlappingStyles(int start, int length) { | |
StyleRange style; | |
if (styleCount == 0) return null; | |
// find the index of the first style for the given offset, use a binary search | |
// to find the index | |
int end = start + length - 1; | |
int high = searchForStyle(start, end); | |
int count = 0; | |
for (int index = high; index < styleCount; index++) { | |
style = styles[index]; | |
int styleEnd = style.start + style.length - 1; | |
if (style.start > end) break; | |
if (styleEnd >= start) count++; | |
} | |
return new Point(high, count); | |
} | |
/** | |
* Returns the background color of a line. Called by StyledText. It is safe to return | |
* the existing Color object since the colors are set and managed by the client. | |
* <p> | |
* | |
* @param index the line index | |
* @return the background color of the line at the given index | |
*/ | |
Color getLineBackground(int index) { | |
return lineBackgrounds[index]; | |
} | |
/** | |
* Returns the style for the character at <code>offset</code>. Called by StyledText. | |
* Returns a new style. Does not return the existing style. | |
* <p> | |
* | |
* @param offset the character position in the text | |
* @return a cloned StyleRange with start == offset and length == 1 if a style is | |
* specified or null if no style is specified | |
*/ | |
StyleRange getStyleRangeAtOffset(int offset) { | |
if (styleCount == 0) return null; | |
Point pt = getOverlappingStyles(offset, 1); | |
if (pt == null || pt.y == 0) return null; | |
StyleRange newStyle = (StyleRange)styles[pt.x].clone(); | |
newStyle.start = offset; | |
newStyle.length = 1; | |
return newStyle; | |
} | |
} |