An SWT StyledText can display text attributes such as bold, italic and strikethrough, alter colors and fonts. What are we to do when we need to go beyond rich text formatting? In this article we present three simple tricks using standard SWT and JFace APIs to create a polished presentation where text attributes alone won't do the job.

Marking The Spot


In order to display something interesting, we need to mark the spot. Normally this is done with a TextPresentation, which specifies character offsets and style ranges. Fortunately the Eclipse APIs give us another mechanism to mark regions of text in an extensible manner: annotations.

The key APIs at our disposal are as follows:

org.eclipse.jface.text.source.Annotation
org.eclipse.jface.text.source.AnnotationModel

Using these APIs we can create annotations and specify their location. An easy place to do this is in your document partitioner. It will be called at the appropriate times to partition your document. By using a RuleBasedPartitionScanner you can modify your rules to create the appropriate annotations.

Now that our document is annotated, we know where we need to draw. To hook up the drawing strategy, we add the following code to the initialization of the SourceViewer:


IAnnotationAccess annotationAccess = new IAnnotationAccess() {
public Object getType(Annotation annotation) {
return annotation.getType();
}
public boolean isMultiLine(Annotation annotation) {
return true;
}
public boolean isTemporary(Annotation annotation) {
return true;
}
};

AnnotationPainter painter = new AnnotationPainter(sourceViewer, annotationAccess);


Now all we have to do is add a drawing strategy to the painter for every kind of annotation that we're interested in drawing. Read on to find out how we do that.

Repainting Characters


Not all fonts can display all characters. This is problematic in an application that is internationalized or one where the user can change the font.

Take for example bullets. Unicode \u2022 can be used to display a solid round bullet with most fonts, but what about an empty one, or a square one? These characters cannot be reliably found in commonly used fonts. The trick we use is to always use the \u2022 character, but repaint it where we want to display something more interesting. By doing that we get the display just right and the text works nicely with copy/paste operations.

To make it work, we create annotations where our bullet characters are in the document. We then hook up a bullet drawing strategy to our painter as follows:


painter.addDrawingStrategy(BulletAnnotation.TYPE, new BulletDrawingStrategy());
painter.addAnnotationType(BulletAnnotation.TYPE, BulletAnnotation.TYPE);
painter.setAnnotationTypeColor(BulletAnnotation.TYPE, getTextWidget().getForeground());

The painter won't invoke our drawing strategy unless the type and type color are also added.

What does our bullet annotation look like? It needs to have enough information for the drawing strategy to know what to draw. In this case the shape of the bullet is dependent on the 'level' of indentation. Here's what I used:



public class BulletAnnotation extends Annotation {

public static final String TYPE = "org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.bullet";

private final int indentLevel;

public BulletAnnotation(int indentLevel) {
super(TYPE, false, Integer.toString(indentLevel));
this.indentLevel = indentLevel;
}

public int getIndentLevel() {
return indentLevel;
}

}


Now we need to implement our drawing strategy. The drawing strategy must 'erase' the existing bullet character and then draw the new bullet shape where the old bullet was.

We erase the previous character by drawing a rectangle the size of the character in the background color:


// erase whatever character was there
gc.fillRectangle(left.x, left.y, right.x - left.x, lineHeight);


then we draw the new shape:


// now paint the bullet
switch (bullet.getIndentLevel()) {
case 1: // round solid bullet
gc.setBackground(color);
gc.fillOval(hcenter - 3, vcenter - 2, 5, 5);
break;
case 2: // round empty bullet
gc.setForeground(color);
gc.drawOval(hcenter - 3, vcenter - 3, 5, 5);
break;
default: // square bullet
gc.setBackground(color);
gc.fillRectangle(hcenter - 3, vcenter - 2, 5, 5);
break;
}


Here's a screenshot showing an example of this technique in use:


Drawing Non-Characters


Sometimes there's a need to display non-characters. For example, browsers display a horizontal line for <hr /> (horizontal rule). By marking the spot with annotations and registering a custom painter, we can do the same thing. Here's the result we're looking for.



To create this effect we put an empty line in the text we're displaying and annotate it with a HorizontalRuleAnnotation. Drawing the annotation is easy:



public void draw(Annotation annotation, GC gc, StyledText textWidget, int offset, int length, Color color) {
if (gc != null) {
final Color foreground = gc.getForeground();

Point left = textWidget.getLocationAtOffset(offset);
Point right = textWidget.getLocationAtOffset(offset + length);
if (left.x > right.x) {
// hack: sometimes linewrapping text widget gives us the wrong x/y for the first character of a line that
// has been wrapped.
left.x = 0;
left.y = right.y;
}
right.x = textWidget.getClientArea().width;

int baseline = textWidget.getBaseline(offset);

int vcenter = left.y + (baseline / 2) + (baseline / 4);

gc.setLineWidth(0); // NOTE: 0 means width is 1 but with optimized performance
gc.setLineStyle(SWT.LINE_SOLID);

left.x += 3;
right.x -= 5;
vcenter -= 2;

if (right.x > left.x) {
// draw the shadow
gc.setForeground(shadowForeground);
gc.drawRectangle(left.x, vcenter, right.x - left.x, 2);

// draw the horizontal rule
gc.setForeground(color);
gc.drawLine(left.x, vcenter, right.x, vcenter);
gc.drawLine(left.x, vcenter, left.x, vcenter + 2);
}

gc.setForeground(foreground);
} else {
textWidget.redrawRange(offset, length, true);
}
}


As before, we hook the drawing strategy up to the painter:


painter.addDrawingStrategy(HorizontalRuleAnnotation.TYPE, new HorizontalRuleDrawingStrategy());
painter.addAnnotationType(HorizontalRuleAnnotation.TYPE, HorizontalRuleAnnotation.TYPE);
painter.setAnnotationTypeColor(HorizontalRuleAnnotation.TYPE, getTextWidget().getForeground());

Conclusion



Eclipse provides some powerful APIs for hooking into the painting of StyledText. Using some simple tricks we can create powerful polished visuals in an SWT user interface. All of these techniques are applied in the Mylyn WikiText project, where you can find source code that works.