blob: 14b2c89920a065b181cfa5829b84365bdcb41fcd [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.custom;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.*;
/**
* A StyledTextRenderer renders the content of a StyledText widget.
* Subclasses can provide a different device (e.g., Display, Printer)
* to render on and implement abstract methods to return resources
* created on that device.
*/
abstract class StyledTextRenderer {
private Device device; // device to render on
protected Font regularFont, boldFont, italicFont, boldItalicFont;
private int tabWidth; // width in pixels of a tab character
private int ascent, descent;
private int lineEndSpaceWidth; // width in pixels of the space used to represent line delimiters
/**
* Creates an instance of <class>StyledTextRenderer</class>.
* </p>
* @param device Device to render on
* @param regularFont Font to use for regular (non-bold) text
* @param leftMargin margin to the left of the text
*/
StyledTextRenderer(Device device, Font regularFont) {
this.device = device;
this.regularFont = regularFont;
}
/**
* Calculates the line height and space width.
*/
void calculateLineHeight() {
GC gc = getGC();
lineEndSpaceWidth = gc.stringExtent(" ").x;
// don't assume that bold and normal fonts have the same height
// fixes bug 41773
Font originalFont = gc.getFont();
FontMetrics metrics = gc.getFontMetrics();
ascent = Math.max(ascent, metrics.getAscent() + metrics.getLeading());
descent = Math.max(descent, metrics.getDescent());
gc.setFont(getFont(SWT.BOLD));
metrics = gc.getFontMetrics();
ascent = Math.max(ascent, metrics.getAscent() + metrics.getLeading());
descent = Math.max(descent, metrics.getDescent());
gc.setFont(getFont(SWT.ITALIC));
metrics = gc.getFontMetrics();
ascent = Math.max(ascent, metrics.getAscent() + metrics.getLeading());
descent = Math.max(descent, metrics.getDescent());
gc.setFont(getFont(SWT.BOLD | SWT.ITALIC));
metrics = gc.getFontMetrics();
ascent = Math.max(ascent, metrics.getAscent() + metrics.getLeading());
descent = Math.max(descent, metrics.getDescent());
gc.setFont(originalFont);
disposeGC(gc);
// clear the font cache
if (boldFont != null) boldFont.dispose();
if (italicFont != null) italicFont.dispose();
if (boldItalicFont != null) boldItalicFont.dispose();
boldFont = italicFont = boldItalicFont = null;
}
/**
* Disposes the resource created by the receiver.
*/
void dispose() {
if (boldFont != null) boldFont.dispose();
if (italicFont != null) italicFont.dispose();
if (boldItalicFont != null) boldItalicFont.dispose();
boldFont = italicFont = boldItalicFont = null;
}
/**
* Dispose the specified GC.
* Allows subclasses to reuse GCs.
* </p>
* @param gc GC to dispose.
*/
protected abstract void disposeGC(GC gc);
/**
* Draws a line of text at the specified location.
* </p>
*
* @param line the line to draw
* @param lineIndex index of the line to draw
* @param paintY y location to draw at
* @param gc GC to draw on
* @param widgetBackground the widget background color.
* Used as the default rendering color.
* @param widgetForeground the widget foreground color.
* Used as the default rendering color.
* @param clearBackground true if the line background should be drawn
* explicitly.
*/
void drawLine(String line, int lineIndex, int paintY, GC gc, Color widgetBackground, Color widgetForeground, boolean clearBackground) {
int lineOffset = getContent().getOffsetAtLine(lineIndex);
int lineLength = line.length();
Point selection = getSelection();
int selectionStart = selection.x;
int selectionEnd = selection.y;
int leftMargin = getLeftMargin();
Color lineBackground = null;
TextLayout layout = getTextLayout(line, lineOffset);
Rectangle client = getClientArea();
StyledTextEvent event = getLineBackgroundData(lineOffset, line);
if (event != null) {
lineBackground = event.lineBackground;
}
if (lineBackground == null) {
lineBackground = widgetBackground;
}
if (clearBackground &&
(isFullLineSelection() == false ||
selectionStart > lineOffset ||
selectionEnd <= lineOffset + lineLength)) {
// draw background if full selection is off or if line is not
// completely selected
gc.setBackground(lineBackground);
gc.setForeground(lineBackground);
gc.fillRectangle(client.x + leftMargin, paintY, client.width, ascent + descent);
}
int paintX = client.x + leftMargin - getHorizontalPixel();
if (selectionStart != selectionEnd) {
Rectangle rect = layout.getLineBounds(0);
drawLineBreakSelection(line, lineOffset, paintX + rect.x + rect.width, paintY, gc);
}
gc.setForeground(widgetForeground);
gc.setBackground(lineBackground);
if (selectionStart == selectionEnd || (selectionEnd <= lineOffset && selectionStart > lineOffset + lineLength - 1)) {
layout.draw(gc, paintX, paintY);
} else {
int start = Math.max(0, selectionStart - lineOffset);
int end = Math.min(lineLength, selectionEnd - lineOffset);
layout.draw(gc, paintX, paintY, start, end - 1, getSelectionForeground(), getSelectionBackground());
}
disposeTextLayout(layout);
}
/**
* Draws the background of the line selection.
* Implemented by subclasses for optional selection rendering.
* </p>
*
* @param line the line to draw
* @param lineOffset offset of the first character in the line.
* Relative to the start of the document.
* @param styles line styles
* @param paintY y location to draw at
* @param gc GC to draw on
* @param bidi the bidi object to use for measuring and rendering text in bidi
* locales. null when not in bidi mode.
*/
protected abstract void drawLineBreakSelection(String line, int lineOffset, int paintX, int paintY, GC gc);
/**
* Returns the visible client area that can be used for rendering.
* </p>
* @return the visible client area that can be used for rendering.
*/
protected abstract Rectangle getClientArea();
/**
* Returns the <class>StyledTextContent</class> to use for line offset
* calculations.
* </p>
* @return the <class>StyledTextContent</class> to use for line offset
* calculations.
*/
protected abstract StyledTextContent getContent();
/**
* Returns the Device that is being rendered on.
* </p>
* @return the Device that is being rendered on.
*/
Device getDevice() {
return device;
}
int getBaseline() {
return ascent;
}
/**
* Returns the text segments that should be treated as if they
* had a different direction than the surrounding text.
* </p>
*
* @param lineOffset offset of the first character in the line.
* 0 based from the beginning of the document.
* @param line text of the line to specify bidi segments for
* @return text segments that should be treated as if they had a
* different direction than the surrounding text. Only the start
* index of a segment is specified, relative to the start of the
* line. Always starts with 0 and ends with the line length.
* @exception IllegalArgumentException <ul>
* <li>ERROR_INVALID_ARGUMENT - if the segment indices returned
* by the listener do not start with 0, are not in ascending order,
* exceed the line length or have duplicates</li>
* </ul>
*/
protected abstract int[] getBidiSegments(int lineOffset, String lineText);
/**
* Returns the Font according with the given style
*/
Font getFont(int style) {
switch (style) {
case SWT.BOLD:
if (boldFont != null) return boldFont;
return boldFont = new Font(device, getFontData(style));
case SWT.ITALIC:
if (italicFont != null) return italicFont;
return italicFont = new Font(device, getFontData(style));
case SWT.BOLD | SWT.ITALIC:
if (boldItalicFont != null) return boldItalicFont;
return boldItalicFont = new Font(device, getFontData(style));
default:
return regularFont;
}
}
FontData[] getFontData(int style) {
FontData[] fontDatas = regularFont.getFontData();
for (int i = 0; i < fontDatas.length; i++) {
fontDatas[i].setStyle(style);
}
return fontDatas;
}
/**
* Returns the GC to use for rendering and measuring.
* Allows subclasses to reuse GCs.
* </p>
* @return the GC to use for rendering and measuring.
*/
protected abstract GC getGC();
/**
* Returns the horizontal scroll position.
* </p>
* @return the horizontal scroll position.
*/
protected abstract int getHorizontalPixel();
protected int getLeftMargin() {
return 0;
}
/**
* Returns the width in pixels of the space used to represent line delimiters.
* @return the width in pixels of the space used to represent line delimiters.
*/
int getLineEndSpaceWidth() {
return lineEndSpaceWidth;
}
/**
* Returns the line background data for the given line or null if
* there is none.
* </p>
* @param lineOffset offset of the line start relative to the start
* of the content.
* @param line line to get line background data for
* @return line background data for the given line. may return null
*/
protected abstract StyledTextEvent getLineBackgroundData(int lineOffset, String line);
/**
* Returns the height in pixels of a line.
* </p>
* @return the height in pixels of a line.
*/
int getLineHeight() {
return ascent + descent;
}
/**
* Returns the line style data for the specified line.
* The lineOffset and line may specify a segment of a logical line stored
* in the <class>StyledTextContent</class> of the widget.
* The returned styles are guaranteed to be at least partially on the
* segment.
* </p>
* @param event the styles for the logical line
* @param lineOffset offset of the line start relative to the start of
* the content.
* @param line line to get line styles for
* @return line style data for the given line segment. Styles may start
* before line start and end after line end but are guaranteed to be at
* least partially on the line.
*/
StyledTextEvent getLineStyleData(StyledTextEvent event, int lineOffset, String line) {
int lineLength = line.length();
if (event.styles != null && getWordWrap()) {
event.styles = getVisualLineStyleData(event.styles, lineOffset, lineLength);
}
if (event.styles == null) {
event.styles = new StyleRange[0];
}
return event;
}
/**
* Returns the line style data for the given line or null if there is
* none. If there is a LineStyleListener but it does not set any styles,
* the StyledTextEvent.styles field will be initialized to an empty
* array.
* </p>
*
* @param lineOffset offset of the line start relative to the start of
* the content.
* @param line line to get line styles for
* @return line style data for the given line. Styles may start before
* line start and end after line end
*/
protected abstract StyledTextEvent getLineStyleData(int lineOffset, String line);
/**
*
*/
protected abstract int getOrientation ();
/**
*
*/
protected int getRightMargin() {
return 0;
}
/**
*
*/
protected abstract Color getSelectionForeground();
/**
*
*/
protected abstract Color getSelectionBackground();
/**
* Returns the widget selection.
* Implemented by subclasses for optional selection rendering.
* </p>
* @return the widget selection.
*/
protected abstract Point getSelection();
/**
* Returns styles for the specified visual (wrapped) line.
* </p>
*
* @param logicalStyles the styles for a logical (unwrapped) line
* @param lineOffset offset of the visual line
* @param lineLength length of the visual line
* @return styles in the logicalStyles array that are at least
* partially on the specified visual line.
*/
StyleRange[] getVisualLineStyleData(StyleRange[] logicalStyles, int lineOffset, int lineLength) {
int lineEnd = lineOffset + lineLength;
int oldStyleCount = logicalStyles.length;
int newStyleCount = 0;
for (int i = 0; i < oldStyleCount; i++) {
StyleRange style = logicalStyles[i];
if (style.start < lineEnd && style.start + style.length > lineOffset) {
newStyleCount++;
}
}
if (newStyleCount != oldStyleCount) {
StyleRange[] newStyles = new StyleRange[newStyleCount];
for (int i = 0, j = 0; i < oldStyleCount; i++) {
StyleRange style = logicalStyles[i];
if (style.start < lineEnd && style.start + style.length > lineOffset) {
newStyles[j++] = logicalStyles[i];
}
}
logicalStyles = newStyles;
}
return logicalStyles;
}
/**
* Returns the word wrap state.
* </p>
* @return true=word wrap is on. false=no word wrap, lines may extend
* beyond the right side of the client area.
*/
protected abstract boolean getWordWrap();
/**
* Returns whether the widget was created with the SWT.FULL_SELECTION style.
* Implemented by subclasses for optional selection rendering.
* </p>
* @return true=the widget is running in full line selection mode,
* false=otherwise
*/
protected abstract boolean isFullLineSelection();
/**
* Calculates the width in pixel of a tab character
* </p>
* @param tabLength number of space characters represented by a tab character.
*/
void setTabLength(int tabLength) {
GC gc = getGC();
StringBuffer tabBuffer = new StringBuffer(tabLength);
for (int i = 0; i < tabLength; i++) {
tabBuffer.append(' ');
}
tabWidth = gc.stringExtent(tabBuffer.toString()).x;
disposeGC(gc);
}
/**
* Returns TextLayout given a line index and an array of styles
*/
TextLayout getTextLayout(String line, int lineOffset) {
TextLayout layout = createTextLayout(lineOffset);
layout.setFont(regularFont);
layout.setAscent(ascent);
layout.setDescent(descent);
layout.setText(line);
layout.setOrientation(getOrientation());
layout.setSegments(getBidiSegments(lineOffset, line));
layout.setTabs(new int[]{tabWidth});
int length = line.length();
StyledTextEvent event = getLineStyleData(lineOffset, line);
StyleRange[] styles = event != null ? event.styles : null;
int lastOffset = 0;
if (styles != null) {
for (int styleIndex = 0; styleIndex < styles.length; styleIndex++) {
StyleRange style = styles[styleIndex];
if (style.isUnstyled()) continue;
int start, end;
if (lineOffset > style.start) {
start = 0;
end = Math.min (length, style.length - lineOffset + style.start);
} else {
start = style.start - lineOffset;
end = Math.min(length, start + style.length);
}
if (start >= length) break;
if (lastOffset != start) {
layout.setStyle(null, lastOffset, start - 1);
}
TextStyle textStyle = new TextStyle(getFont(style.fontStyle), style.foreground, style.background);
layout.setStyle(textStyle, start, end - 1);
lastOffset = end;
}
}
if (lastOffset != length) layout.setStyle(null, lastOffset, length);
return layout;
}
TextLayout createTextLayout(int lineOffset) {
return new TextLayout(device);
}
void disposeTextLayout (TextLayout layout) {
layout.dispose();
}
}