blob: fba729adaf5a2d9f188e71a181c5c1f538364a59 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 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.jface.text.source;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPaintPositionManager;
import org.eclipse.jface.text.IPainter;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextPresentationListener;
import org.eclipse.jface.text.ITextViewerExtension2;
import org.eclipse.jface.text.ITextViewerExtension3;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextPresentation;
/**
* Paints annotations provided by an annotation model as squiggly lines and/or
* highlighted onto an associated source viewer.
* Clients usually instantiate and configure objects of this class.
*
* @since 2.1
*/
public class AnnotationPainter implements IPainter, PaintListener, IAnnotationModelListener, ITextPresentationListener {
/**
* The presentation information (decoration) for an annotation. Each such
* object represents one squiggly.
*/
private static class Decoration {
/** The position of this decoration */
private Position fPosition;
/** The color of this decoration */
private Color fColor;
/**
* The annotation's layer
* @since 3.0
*/
private int fLayer;
}
/** Indicates whether this painter is active */
private boolean fIsActive= false;
/** Indicates whether this painter is managing decorations */
private boolean fIsPainting= false;
/** Indicates whether this painter is setting its annotation model */
private boolean fIsSettingModel= false;
/** The associated source viewer */
private ISourceViewer fSourceViewer;
/** The cached widget of the source viewer */
private StyledText fTextWidget;
/** The annotation model providing the annotations to be drawn */
private IAnnotationModel fModel;
/** The annotation access */
private IAnnotationAccess fAnnotationAccess;
/** The list of decorations */
private List fDecorations= new ArrayList();
/**
* The list of highlighted decorations.
* @since 3.0
*/
private List fHighlightedDecorations= new ArrayList();
/** The internal color table */
private Map fColorTable= new HashMap();
/** The list of types of annotations that are painted by this painter */
private Set fAnnotationTypes= new HashSet();
/**
* The list of types of annotations that are highlighted by this painter.
* @since 3.0
*/
private Set fHighlightAnnotationTypes= new HashSet();
/**
* The range in which all highlight annotations can be found.
* @since 3.0
*/
private Position fHighlightAnnotationRange= new Position(0);
/**
* Creates a new annotation painter for the given source viewer and with the given
* annotation access. The painter is uninitialized, i.e. no annotation types are configured
* to be painted.
*
* @param sourceViewer the source viewer for this painter
* @param access the annotation access for this painter
*/
public AnnotationPainter(ISourceViewer sourceViewer, IAnnotationAccess access) {
fSourceViewer= sourceViewer;
fAnnotationAccess= access;
fTextWidget= sourceViewer.getTextWidget();
}
/**
* Returns whether this painter has to draw any squiggle.
*
* @return <code>true</code> if there are squiggles to be drawn, <code>false</code> otherwise
*/
private boolean hasDecorations() {
return !fDecorations.isEmpty();
}
/**
* Enables painting. This painter registers a paint listener with the
* source viewer's widget.
*/
private void enablePainting() {
if (!fIsPainting && hasDecorations()) {
fIsPainting= true;
fTextWidget.addPaintListener(this);
handleDrawRequest(null);
}
}
/**
* Disables painting, if is has previously been enabled. Removes
* any paint listeners registered with the source viewer's widget.
*
* @param redraw <code>true</code> if the widget should be redrawn after disabling
*/
private void disablePainting(boolean redraw) {
if (fIsPainting) {
fIsPainting= false;
fTextWidget.removePaintListener(this);
if (redraw && hasDecorations())
handleDrawRequest(null);
}
}
/**
* Sets the annotation model for this painter. Registers this painter
* as listener of the give model, if the model is not <code>null</code>.
*
* @param model the annotation model
*/
private void setModel(IAnnotationModel model) {
if (fModel != model) {
if (fModel != null)
fModel.removeAnnotationModelListener(this);
fModel= model;
if (fModel != null) {
try {
fIsSettingModel= true;
fModel.addAnnotationModelListener(this);
} finally {
fIsSettingModel= false;
}
}
}
}
/**
* Updates the set of decorations based on the current state of
* the painter's annotation model.
*/
private synchronized void catchupWithModel() {
if (fDecorations != null && fHighlightAnnotationTypes != null) {
fDecorations.clear();
fHighlightedDecorations.clear();
int highlightAnnotationRangeStart= Integer.MAX_VALUE;
int highlightAnnotationRangeEnd= -1;
if (fModel != null) {
Iterator e= fModel.getAnnotationIterator();
while (e.hasNext()) {
Annotation annotation= (Annotation) e.next();
Object annotationType= fAnnotationAccess.getType(annotation);
if (annotationType == null)
continue;
Color color= null;
boolean isHighlighting= fHighlightAnnotationTypes.contains(annotationType);
boolean isDrawingSquiggles= fAnnotationTypes.contains(annotationType);
if (isDrawingSquiggles || isHighlighting)
color= (Color) fColorTable.get(annotationType);
if (color != null) {
Position position= fModel.getPosition(annotation);
if (position == null || position.isDeleted())
continue;
Decoration pp= new Decoration();
pp.fPosition= position;
pp.fColor= color;
if (fAnnotationAccess instanceof IAnnotationAccessExtension) {
IAnnotationAccessExtension extension= (IAnnotationAccessExtension) fAnnotationAccess;
pp.fLayer= extension.getLayer(annotation);
} else {
pp.fLayer= IAnnotationAccessExtension.DEFAULT_LAYER;
}
if (isDrawingSquiggles)
fDecorations.add(pp);
if (isHighlighting) {
fHighlightedDecorations.add(pp);
highlightAnnotationRangeStart= Math.min(highlightAnnotationRangeStart, position.offset);
highlightAnnotationRangeEnd= Math.max(highlightAnnotationRangeEnd, position.offset + position.length);
}
}
}
if (!fHighlightedDecorations.isEmpty()) {
fHighlightAnnotationRange.offset= highlightAnnotationRangeStart;
fHighlightAnnotationRange.length= highlightAnnotationRangeEnd - highlightAnnotationRangeStart;
}
}
}
}
/**
* Recomputes the squiggles to be drawn and redraws them.
*/
private void updatePainting() {
disablePainting(true);
catchupWithModel();
invalidateTextPresentation();
enablePainting();
}
private void invalidateTextPresentation() {
if (fSourceViewer instanceof ITextViewerExtension2) {
IRegion r= getWidgetRange(fHighlightAnnotationRange);
if (r != null)
((ITextViewerExtension2)fSourceViewer).invalidateTextPresentation(r.getOffset(), r.getLength());
} else {
fSourceViewer.invalidateTextPresentation();
}
}
/*
* @see ITextPresentationListener#applyTextPresentation(TextPresentation, IRegion)
* @since 3.0
*/
public synchronized void applyTextPresentation(TextPresentation tp, IRegion region) {
for (Iterator iter= fHighlightedDecorations.iterator(); iter.hasNext();) {
Decoration pp = (Decoration)iter.next();
Position p= pp.fPosition;
if (!fSourceViewer.overlapsWithVisibleRegion(p.offset, p.length))
continue;
if (p.getOffset() + p.getLength() >= region.getOffset() && region.getOffset() + region.getLength() > p.getOffset())
tp.mergeStyleRange(new StyleRange(p.getOffset(), p.getLength(), null, pp.fColor));
}
}
/*
* @see IAnnotationModelListener#modelChanged(IAnnotationModel)
*/
public void modelChanged(final IAnnotationModel model) {
if (fTextWidget != null && !fTextWidget.isDisposed()) {
if (fIsSettingModel) {
// inside the ui thread -> no need for posting
updatePainting();
} else {
Display d= fTextWidget.getDisplay();
if (d != null) {
d.asyncExec(new Runnable() {
public void run() {
if (fTextWidget != null && !fTextWidget.isDisposed())
updatePainting();
}
});
}
}
}
}
/**
* Sets the color in which the squiggly for the given annotation type should be drawn.
*
* @param annotationType the annotation type
* @param color the color
*/
public void setAnnotationTypeColor(Object annotationType, Color color) {
if (color != null)
fColorTable.put(annotationType, color);
else
fColorTable.remove(annotationType);
}
/**
* Adds the given annotation type to the list of annotation types whose
* annotations should be painted by this painter. If the annotation type
* is already in this list, this method is without effect.
*
* @param annotationType the annotation type
*/
public void addAnnotationType(Object annotationType) {
fAnnotationTypes.add(annotationType);
}
/**
* Adds the given annotation type to the list of annotation types whose
* annotations should be highlighted this painter. If the annotation type
* is already in this list, this method is without effect.
*
* @param annotationType the annotation type
* @since 3.0
*/
public void addHighlightAnnotationType(Object annotationType) {
fHighlightAnnotationTypes.add(annotationType);
}
/**
* Removes the given annotation type from the list of annotation types whose
* annotations are painted by this painter. If the annotation type is not
* in this list, this method is wihtout effect.
*
* @param annotationType the annotation type
*/
public void removeAnnotationType(Object annotationType) {
fAnnotationTypes.remove(annotationType);
}
/**
* Removes the given annotation type from the list of annotation types whose
* annotations are highlighted by this painter. If the annotation type is not
* in this list, this method is wihtout effect.
*
* @param annotationType the annotation type
* @since 3.0
*/
public void removeHighlightAnnotationType(Object annotationType) {
fHighlightAnnotationTypes.remove(annotationType);
}
/**
* Clears the list of annotation types whose annotations are
* painted by this painter.
*/
public void removeAllAnnotationTypes() {
fAnnotationTypes.clear();
fHighlightAnnotationTypes.clear();
}
/**
* Returns whether the list of annotation types whose annotations are painted
* by this painter contains at least on element.
*
* @return <code>true</code> if there is an annotation type whose annotations are painted
*/
public boolean isPaintingAnnotations() {
return !fAnnotationTypes.isEmpty() || !fHighlightAnnotationTypes.isEmpty();
}
/*
* @see IPainter#dispose()
*/
public void dispose() {
if (fColorTable != null)
fColorTable.clear();
fColorTable= null;
if (fAnnotationTypes != null)
fAnnotationTypes.clear();
fAnnotationTypes= null;
if (fHighlightAnnotationTypes != null)
fHighlightAnnotationTypes.clear();
fHighlightAnnotationTypes= null;
fTextWidget= null;
fSourceViewer= null;
fAnnotationAccess= null;
fModel= null;
fDecorations= null;
fHighlightedDecorations= null;
}
/**
* Returns the document offset of the upper left corner of the source viewer's viewport,
* possibly including partially visible lines.
*
* @return the document offset if the upper left corner of the viewport
*/
private int getInclusiveTopIndexStartOffset() {
if (fTextWidget != null && !fTextWidget.isDisposed()) {
int top= fSourceViewer.getTopIndex();
if ((fTextWidget.getTopPixel() % fTextWidget.getLineHeight()) != 0)
top--;
try {
IDocument document= fSourceViewer.getDocument();
return document.getLineOffset(top);
} catch (BadLocationException ex) {
}
}
return -1;
}
/*
* @see PaintListener#paintControl(PaintEvent)
*/
public void paintControl(PaintEvent event) {
if (fTextWidget != null)
handleDrawRequest(event.gc);
}
/**
* Handles the request to draw the annotations using the given gaphical context.
*
* @param gc the graphical context
*/
private void handleDrawRequest(GC gc) {
if (fTextWidget == null) {
// is already disposed
return;
}
int vOffset= getInclusiveTopIndexStartOffset();
// http://bugs.eclipse.org/bugs/show_bug.cgi?id=17147
int vLength= fSourceViewer.getBottomIndexEndOffset() + 1;
for (int layer= 0, maxLayer= 1; layer < maxLayer; layer++) {
for (Iterator e = fDecorations.iterator(); e.hasNext();) {
Decoration pp = (Decoration) e.next();
maxLayer= Math.max(maxLayer, pp.fLayer + 1); // dynamically update layer maximum
if (pp.fLayer != layer) // wrong layer: skip annotation
continue;
Position p= pp.fPosition;
if (p.overlapsWith(vOffset, vLength)) {
IDocument document= fSourceViewer.getDocument();
try {
int startLine= document.getLineOfOffset(p.getOffset());
int lastInclusive= Math.max(p.getOffset(), p.getOffset() + p.getLength() - 1);
int endLine= document.getLineOfOffset(lastInclusive);
for (int i= startLine; i <= endLine; i++) {
IRegion line= document.getLineInformation(i);
int paintStart= Math.max(line.getOffset(), p.getOffset());
int paintEnd= Math.min(line.getOffset() + line.getLength(), p.getOffset() + p.getLength());
if (paintEnd > paintStart) {
// otherwise inside a line delimiter
IRegion widgetRange= getWidgetRange(new Position(paintStart, paintEnd - paintStart));
if (widgetRange != null)
draw(gc, widgetRange.getOffset(), widgetRange.getLength(), pp.fColor);
}
}
} catch (BadLocationException x) {
// just don't draw it
}
}
}
}
}
/**
* Returns the widget region that corresponds to the given region in the
* viewer's document.
*
* @param p the region in the viewer's document
* @return the corresponding widget region
*/
private IRegion getWidgetRange(Position p) {
if (p == null)
return null;
if (fSourceViewer instanceof ITextViewerExtension3) {
ITextViewerExtension3 extension= (ITextViewerExtension3) fSourceViewer;
return extension.modelRange2WidgetRange(new Region(p.getOffset(), p.getLength()));
} else {
IRegion region= fSourceViewer.getVisibleRegion();
int offset= region.getOffset();
int length= region.getLength();
if (p.overlapsWith(offset , length)) {
int p1= Math.max(offset, p.getOffset());
int p2= Math.min(offset + length, p.getOffset() + p.getLength());
return new Region(p1 - offset, p2 - p1);
}
}
return null;
}
/**
* Computes an array of alternating x and y values which are the corners of the squiggly line of the
* given height between the given end points.
*
* @param left the left end point
* @param right the right end point
* @param height the height of the squiggly line
* @return the array of alternating x and y values which are the corners of the squiggly line
*/
private int[] computePolyline(Point left, Point right, int height) {
final int WIDTH= 4; // must be even
final int HEIGHT= 2; // can be any number
// final int MINPEEKS= 2; // minimal number of peeks
int peeks= (right.x - left.x) / WIDTH;
// if (peeks < MINPEEKS) {
// int missing= (MINPEEKS - peeks) * WIDTH;
// left.x= Math.max(0, left.x - missing/2);
// peeks= MINPEEKS;
// }
int leftX= left.x;
// compute (number of point) * 2
int length= ((2 * peeks) + 1) * 2;
if (length < 0)
return new int[0];
int[] coordinates= new int[length];
// cache peeks' y-coordinates
int bottom= left.y + height - 1;
int top= bottom - HEIGHT;
// populate array with peek coordinates
for (int i= 0; i < peeks; i++) {
int index= 4 * i;
coordinates[index]= leftX + (WIDTH * i);
coordinates[index+1]= bottom;
coordinates[index+2]= coordinates[index] + WIDTH/2;
coordinates[index+3]= top;
}
// the last down flank is missing
coordinates[length-2]= left.x + (WIDTH * peeks);
coordinates[length-1]= bottom;
return coordinates;
}
/**
* Draws a squiggly line of the given length start at the given offset in the
* given color.
*
* @param gc the grahical context
* @param offset the offset of the line
* @param length the length of the line
* @param color the color of the line
*/
private void draw(GC gc, int offset, int length, Color color) {
if (gc != null) {
Point left= fTextWidget.getLocationAtOffset(offset);
Point right= fTextWidget.getLocationAtOffset(offset + length);
gc.setForeground(color);
int[] polyline= computePolyline(left, right, gc.getFontMetrics().getHeight());
gc.drawPolyline(polyline);
} else {
fTextWidget.redrawRange(offset, length, true);
}
}
/*
* @see IPainter#deactivate(boolean)
*/
public void deactivate(boolean redraw) {
if (fIsActive) {
fIsActive= false;
disablePainting(redraw);
setModel(null);
catchupWithModel();
}
}
/*
* @see IPainter#paint(int)
*/
public void paint(int reason) {
if (fSourceViewer.getDocument() == null) {
deactivate(false);
return;
}
if (!fIsActive) {
IAnnotationModel model= fSourceViewer.getAnnotationModel();
if (model != null) {
fIsActive= true;
setModel(fSourceViewer.getAnnotationModel());
}
} else if (CONFIGURATION == reason || INTERNAL == reason)
updatePainting();
}
/*
* @see org.eclipse.jface.text.IPainter#setPositionManager(org.eclipse.jface.text.IPaintPositionManager)
*/
public void setPositionManager(IPaintPositionManager manager) {
}
}