blob: c14d2ad957915c64db867a5d0b58b599443c15c7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.text.revisions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.revisions.Revision;
import org.eclipse.jface.text.revisions.RevisionInformation;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IAnnotationHover;
import org.eclipse.jface.text.source.IAnnotationHoverExtension;
import org.eclipse.jface.text.source.IAnnotationHoverExtension2;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.IAnnotationModelListener;
import org.eclipse.jface.text.source.IChangeRulerColumn;
import org.eclipse.jface.text.source.ILineDiffer;
import org.eclipse.jface.text.source.ILineRange;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRulerColumn;
import org.eclipse.jface.text.source.LineRange;
import org.eclipse.jface.internal.text.JFaceTextUtil;
/**
* A strategy for painting the live annotate colors onto the vertical ruler column. It also manages
* the revision hover.
*
* @since 3.2
*/
public final class RevisionPainter {
/** Tells whether this class is in debug mode. */
private static boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.jface.text.source/debug/RevisionRulerColumn")); //$NON-NLS-1$//$NON-NLS-2$
/**
* The annotations created to show a revision in the overview ruler.
*/
private static final class RevisionAnnotation extends Annotation {
public RevisionAnnotation(String text) {
super("org.eclipse.ui.workbench.texteditor.revisionAnnotation", false, text); //$NON-NLS-1$
}
}
/**
* The color tool manages revision colors and computes shaded colors based on the relative age
* and author of a revision.
*/
private final class ColorTool {
/**
* The average perceived intensity of a base color. 0 means black, 1 means white. A base
* revision color perceived as light such as yellow will be darkened, while colors perceived
* as dark such as blue will be lightened up.
*/
private static final float AVERAGE_INTENSITY= 0.6f;
/**
* The maximum shading in [0, 1] - this is the shade that the most recent revision will
* receive.
*/
private static final float MAX_SHADING= 0.8f;
/**
* The minimum shading in [0, 1] - this is the shade that the oldest revision will receive.
*/
private static final float MIN_SHADING= 0.3f;
/**
* The shade for the focus boxes.
*/
private static final float FOCUS_COLOR_SHADING= 0.9f;
/**
* A list of {@link Long}, storing the age of each revision in a sorted list.
*/
private List fRevisions;
/**
* The stored shaded colors.
*/
private final Map fColors= new HashMap();
/**
* The st
*/
private final Map fFocusColors= new HashMap();
/**
* Sets the revision information, which is needed to compute the relative age of a revision.
*
* @param info the new revision info, <code>null</code> for none.
*/
public void setInfo(RevisionInformation info) {
fRevisions= null;
fColors.clear();
fFocusColors.clear();
if (info == null)
return;
List revisions= new ArrayList();
for (Iterator it= info.getRevisions().iterator(); it.hasNext();) {
Revision revision= (Revision) it.next();
revisions.add(new Long(computeAge(revision)));
}
Collections.sort(revisions);
fRevisions= revisions;
}
private RGB adaptColorToAge(Revision revision, RGB rgb, boolean focus) {
long age= computeAge(revision);
// relative age: newest is 0, oldest is 1
// if there is only one revision, use an intermediate value to avoid extreme coloring
int size= fRevisions.size();
float relativeAge= size == 1 ? 0.5f : (float) fRevisions.indexOf(new Long(age)) / (size - 1);
return getShadedColor(rgb, 1 - relativeAge, focus);
}
private RGB getShadedColor(RGB color, float scale, boolean focus) {
Assert.isLegal(scale >= 0.0);
Assert.isLegal(scale <= 1.0);
RGB background= getBackground().getRGB();
// focus coloring
if (focus) {
scale-= FOCUS_COLOR_SHADING;
if (scale < 0) {
background= new RGB(255 - background.red, 255 - background.green, 255 - background.blue);
scale= -scale;
}
}
color= normalizeColor(color);
// normalize to lie within [MIN_SHADING, MAX_SHADING]
scale= (MAX_SHADING - MIN_SHADING) * scale + MIN_SHADING;
return interpolate(color, background, scale);
}
/**
* Normalizes a color in its perceived lightness.
*
* @param color the color to normalize
* @return a normalized version of <code>color</code>
*/
private RGB normalizeColor(RGB color) {
/*
* Normalize the gray value (this helps e.g. yellow colors to not look fainter than red
* colors). The gray level of the resulting color shall lie within [MIN_SHADING,
* MAX_SHADING].
*/
float[] hsi= toHSI(color);
float psychoFactor= AVERAGE_INTENSITY - grayLevel(color);
float weight= 0.4f; // found by trial and error
hsi[2]= Math.max(0, Math.min(1.0f, hsi[2] + psychoFactor * weight));
color= fromHSI(hsi);
return color;
}
/**
* Returns the human-perceived gray value in which the given color would be drawn in
* gray-scale in [0.0, 1.0].
*
* @param rgb the color
* @return the gray-scale value
*/
private float grayLevel(RGB rgb) {
if (rgb.red == rgb.green && rgb.green == rgb.blue)
return rgb.red;
// return Math.min(1f, (0.299f * rgb.red + 0.587f * rgb.green + 0.114f * rgb.blue + 0.5f) / 255f);
return Math.min(1f, (0.2126f * rgb.red + 0.7152f * rgb.green + 0.0722f * rgb.blue + 0.5f) / 255f);
}
/**
* Converts an {@link RGB} to HSI.
*
* @param color the color to convert
* @return the HSI float array of length 3
*/
private float[] toHSI(RGB color) {
float r = color.red / 255f;
float g = color.green / 255f;
float b = color.blue / 255f;
float max = Math.max(Math.max(r, g), b);
float min = Math.min(Math.min(r, g), b);
float delta = max - min;
float maxPlusMin= max + min;
float intensity = maxPlusMin / 2;
float saturation= intensity < 0.5 ? delta / maxPlusMin : delta / (2 - maxPlusMin);
float hue = 0;
if (delta != 0) {
if (r == max) {
hue = (g - b) / delta;
} else {
if (g == max) {
hue = 2 + (b - r) / delta;
} else {
hue = 4 + (r - g) / delta;
}
}
hue *= 60;
if (hue < 0) hue += 360;
}
return new float[] {hue, saturation, intensity};
}
/**
* Converts a HSI float array of length 3 to an RGB.
*
* @param hsi the HSI values
* @return the RGB corresponding to the HSI spec
*/
private RGB fromHSI(float[] hsi) {
float r, g, b;
float hue= hsi[0];
float saturation= hsi[1];
float intensity= hsi[2];
if (saturation == 0) {
r = g = b = intensity;
} else {
float temp2= intensity < 0.5f ? intensity * (1.0f + saturation) : (intensity + saturation) - (intensity * saturation);
float temp1= 2f * intensity - temp2;
if (hue == 360) hue = 0;
hue /= 360;
r= hue2RGB(temp1, temp2, hue + 1f/3f);
g= hue2RGB(temp1, temp2, hue);
b= hue2RGB(temp1, temp2, hue - 1f/3f);
}
int red = (int)(r * 255 + 0.5);
int green = (int)(g * 255 + 0.5);
int blue = (int)(b * 255 + 0.5);
return new RGB(red, green, blue);
}
float hue2RGB(float t1, float t2, float hue) {
if (hue < 0)
hue += 1;
if (hue > 1)
hue -= 1;
if (6f * hue < 1)
return t1 +(t2 - t1) * 6f * hue;
if (2f * hue < 1)
return t2;
if (3f * hue < 2)
return t1 + (t2 - t1) * (2f/3f - hue) * 6f;
return t1;
}
/**
* Returns a specification of a color that lies between the given foreground and background
* color using the given scale factor. A <code>scale</code> factor of 1.0 will produce a
* color equal to <code>fg</code>, while a <code>scale</code> of 0.0 will produce one
* equal to <code>bg</code>.
*
* @param fg the foreground color
* @param bg the background color
* @param scale the scale factor
* @return the interpolated color
*/
private RGB interpolate(RGB fg, RGB bg, float scale) {
return new RGB(
(int) ((1.0f - scale) * fg.red + scale * bg.red),
(int) ((1.0f - scale) * fg.green + scale * bg.green),
(int) ((1.0f - scale) * fg.blue + scale * bg.blue)
);
}
private long computeAge(Revision revision) {
return revision.getDate().getTime();
}
/**
* Returns the color for a revision based on relative age and author.
*
* @param revision the revision
* @param focus <code>true</code> to return the focus color
* @return the color for a revision
*/
public RGB getColor(Revision revision, boolean focus) {
Map map= focus ? fFocusColors : fColors;
RGB color= (RGB) map.get(revision);
if (color == null) {
color= adaptColorToAge(revision, revision.getColor(), focus);
map.put(revision, color);
}
return color;
}
}
/**
* Handles all the mouse interaction in this line number ruler column.
*/
private class MouseHandler implements MouseMoveListener, MouseTrackListener, Listener {
/*
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
public void mouseUp(MouseEvent event) {
}
/*
* @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
*/
public void handleEvent(Event event) {
Assert.isTrue(event.type == SWT.MouseWheel);
handleMouseWheel(event);
}
/*
* @see org.eclipse.swt.events.MouseTrackListener#mouseEnter(org.eclipse.swt.events.MouseEvent)
*/
public void mouseEnter(MouseEvent e) {
onEnter();
updateFocusLine(toDocumentLineNumber(e.y));
}
/*
* @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent)
*/
public void mouseExit(MouseEvent e) {
updateFocusLine(-1);
onExit();
}
/*
* @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
*/
public void mouseHover(MouseEvent e) {
onHover();
}
/*
* @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent)
*/
public void mouseMove(MouseEvent e) {
updateFocusLine(toDocumentLineNumber(e.y));
}
}
/**
* Internal listener class that will update the ruler when the underlying model changes.
*/
private class AnnotationListener implements IAnnotationModelListener {
/*
* @see org.eclipse.jface.text.source.IAnnotationModelListener#modelChanged(org.eclipse.jface.text.source.IAnnotationModel)
*/
public void modelChanged(IAnnotationModel model) {
fChangeRegions= null;
postRedraw();
}
}
/**
* The information control creator.
*/
private static final class InformationControlCreator extends AbstractReusableInformationControlCreator {
/*
* @see org.eclipse.jface.internal.text.revisions.AbstractReusableInformationControlCreator#doCreateInformationControl(org.eclipse.swt.widgets.Shell)
*/
protected IInformationControl doCreateInformationControl(Shell parent) {
int shellStyle= SWT.RESIZE | SWT.TOOL;
int style= SWT.NONE /*| SWT.V_SCROLL | SWT.H_SCROLL*/;
if (BrowserInformationControl.isAvailable(parent))
return new BrowserInformationControl(parent, shellStyle, style, null, true);
return new DefaultInformationControl(parent, shellStyle, style, null);
}
}
/**
* The revision hover displays information about the currently selected revision.
*/
private final class RevisionHover implements IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2 {
/*
* @see org.eclipse.jface.text.source.IAnnotationHover#getHoverInfo(org.eclipse.jface.text.source.ISourceViewer,
* int)
*/
public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) {
Object info= getHoverInfo(sourceViewer, getHoverLineRange(sourceViewer, lineNumber), 0);
return info == null ? null : info.toString();
}
/*
* @see org.eclipse.jface.text.source.IAnnotationHoverExtension#getHoverControlCreator()
*/
public IInformationControlCreator getHoverControlCreator() {
return new InformationControlCreator();
}
/*
* @see org.eclipse.jface.text.source.IAnnotationHoverExtension#canHandleMouseCursor()
*/
public boolean canHandleMouseCursor() {
return false;
}
/*
* @see org.eclipse.jface.text.source.IAnnotationHoverExtension2#canHandleMouseWheel()
*/
public boolean canHandleMouseWheel() {
return true;
}
/*
* @see org.eclipse.jface.text.source.IAnnotationHoverExtension#getHoverInfo(org.eclipse.jface.text.source.ISourceViewer,
* org.eclipse.jface.text.source.ILineRange, int)
*/
public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) {
ChangeRegion region= getChangeRegion(lineRange.getStartLine());
Object info= region == null ? null : region.getRevision().getHoverInfo();
if (info instanceof String) {
info= addCSSToHTMLFragment((String) info);
}
return info;
}
/**
* Adds a HTML header and CSS info if <code>html</code> is only an HTML fragment (has no
* &lt;html&gt; section).
*
* @param html the html / text produced by a revision
* @return modified html
*/
private Object addCSSToHTMLFragment(String html) {
int max= Math.min(100, html.length());
if (html.substring(0, max).indexOf("<html>") != -1) //$NON-NLS-1$
// there is already a header
return html;
StringBuffer info= new StringBuffer(512 + html.length());
HTMLPrinter.insertPageProlog(info, 0, getStyles());
info.append(html);
HTMLPrinter.addPageEpilog(info);
return info.toString();
}
private String getStyles() {
return "P.Code {\n" + //$NON-NLS-1$
" display: block;\n" + //$NON-NLS-1$
" text-align: left;\n" + //$NON-NLS-1$
" text-indent: 0.00pt;\n" + //$NON-NLS-1$
" margin-top: 0.000000pt;\n" + //$NON-NLS-1$
" margin-bottom: 0.000000pt;\n" + //$NON-NLS-1$
" margin-right: 0.000000pt;\n" + //$NON-NLS-1$
" margin-left: 0pt;\n" + //$NON-NLS-1$
" font-size: 9.000000pt;\n" + //$NON-NLS-1$
" font-weight: medium;\n" + //$NON-NLS-1$
" font-style: Regular;\n" + //$NON-NLS-1$
" color: #4444CC;\n" + //$NON-NLS-1$
" text-decoration: none;\n" + //$NON-NLS-1$
" vertical-align: baseline;\n" + //$NON-NLS-1$
" text-transform: none;\n" + //$NON-NLS-1$
" font-family: \"Courier New\";\n" + //$NON-NLS-1$
"}\n" + //$NON-NLS-1$
"H6.CaptionFigColumn {\n" + //$NON-NLS-1$
" display: block;\n" + //$NON-NLS-1$
" text-align: left;\n" + //$NON-NLS-1$
" text-indent: 0.000000pt;\n" + //$NON-NLS-1$
" margin-top: 3.000000pt;\n" + //$NON-NLS-1$
" margin-bottom: 11.000000pt;\n" + //$NON-NLS-1$
" margin-right: 0.000000pt;\n" + //$NON-NLS-1$
" margin-left: 0.000000pt;\n" + //$NON-NLS-1$
" font-size: 9.000000pt;\n" + //$NON-NLS-1$
" font-weight: medium;\n" + //$NON-NLS-1$
" font-style: Italic;\n" + //$NON-NLS-1$
" color: #000000;\n" + //$NON-NLS-1$
" text-decoration: none;\n" + //$NON-NLS-1$
" vertical-align: baseline;\n" + //$NON-NLS-1$
" text-transform: none;\n" + //$NON-NLS-1$
" font-family: \"Arial\";\n" + //$NON-NLS-1$
"}\n" + //$NON-NLS-1$
"P.Note {\n" + //$NON-NLS-1$
" display: block;\n" + //$NON-NLS-1$
" text-align: left;\n" + //$NON-NLS-1$
" text-indent: 0pt;\n" + //$NON-NLS-1$
" margin-top: 19.500000pt;\n" + //$NON-NLS-1$
" margin-bottom: 19.500000pt;\n" + //$NON-NLS-1$
" margin-right: 0.000000pt;\n" + //$NON-NLS-1$
" margin-left: 30pt;\n" + //$NON-NLS-1$
" font-size: 11.000000pt;\n" + //$NON-NLS-1$
" font-weight: medium;\n" + //$NON-NLS-1$
" font-style: Italic;\n" + //$NON-NLS-1$
" color: #000000;\n" + //$NON-NLS-1$
" text-decoration: none;\n" + //$NON-NLS-1$
" vertical-align: baseline;\n" + //$NON-NLS-1$
" text-transform: none;\n" + //$NON-NLS-1$
" font-family: \"Arial\";\n" + //$NON-NLS-1$
"}\n" + //$NON-NLS-1$
"EM.UILabel {\n" + //$NON-NLS-1$
" font-weight: Bold;\n" + //$NON-NLS-1$
" font-style: Regular;\n" + //$NON-NLS-1$
" text-decoration: none;\n" + //$NON-NLS-1$
" vertical-align: baseline;\n" + //$NON-NLS-1$
" text-transform: none;\n" + //$NON-NLS-1$
"}\n" + //$NON-NLS-1$
"EM.CodeName {\n" + //$NON-NLS-1$
" font-weight: Bold;\n" + //$NON-NLS-1$
" font-style: Regular;\n" + //$NON-NLS-1$
" text-decoration: none;\n" + //$NON-NLS-1$
" vertical-align: baseline;\n" + //$NON-NLS-1$
" text-transform: none;\n" + //$NON-NLS-1$
" font-family:\"Courier New\";\n" + //$NON-NLS-1$
"}\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"/* following font face declarations need to be removed for DBCS */\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt {font-family: Arial, Helvetica, sans-serif; color: #000000}\n" + //$NON-NLS-1$
"pre { font-family: \"Courier New\", monospace}\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"/* end font face declarations */\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"/* following font size declarations should be OK for DBCS */\n" + //$NON-NLS-1$
"body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt {font-size: 8pt; }\n" + //$NON-NLS-1$
"pre { font-size: 8pt}\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"/* end font size declarations */\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"body { overflow: auto; margin-top: 0; margin-bottom: 4; margin-left: 3; margin-right: 0 }\n" + //$NON-NLS-1$
"h1 { font-size: 10pt; margin-top: 5; margin-bottom: 1 } \n" + //$NON-NLS-1$
"h2 { font-size: 10pt; margin-top: 25; margin-bottom: 3 }\n" + //$NON-NLS-1$
"h3 { font-size: 10pt; margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$
"h4 { font-size: 10pt; margin-top: 20; margin-bottom: 3; font-style: italic }\n" + //$NON-NLS-1$
"h5 { font-size: 8pt; margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$
"p { margin-top: 10px; margin-bottom: 10px }\n" + //$NON-NLS-1$
"pre { margin-left: 6 }\n" + //$NON-NLS-1$
"a:link { color: #0000FF }\n" + //$NON-NLS-1$
"a:hover { color: #000080 }\n" + //$NON-NLS-1$
"a:visited { text-decoration: underline }\n" + //$NON-NLS-1$
"ul { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$
"li { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$
"li p { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$
"ol { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$
"dl { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$
"dt { margin-top: 0; margin-bottom: 0; font-weight: bold }\n" + //$NON-NLS-1$
"dd { margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$
"strong { font-weight: bold}\n" + //$NON-NLS-1$
"em { font-style: italic}\n" + //$NON-NLS-1$
"var { font-style: italic}\n" + //$NON-NLS-1$
"div.revision { border-left-style: solid; border-left-width: thin; \n" + //$NON-NLS-1$
" border-left-color: #7B68EE; padding-left:5 }\n" + //$NON-NLS-1$
"th { font-weight: bold }\n" + //$NON-NLS-1$
""; //$NON-NLS-1$
}
/*
* @see org.eclipse.jface.text.source.IAnnotationHoverExtension#getHoverLineRange(org.eclipse.jface.text.source.ISourceViewer,
* int)
*/
public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) {
ChangeRegion region= getChangeRegion(lineNumber);
return region == null ? null : new LineRange(lineNumber, 1);
}
}
/* Listeners and helpers. */
/** The shared color provider. */
private final ISharedTextColors fSharedColors;
/** The color tool. */
private final ColorTool fColorTool= new ColorTool();
/** The mouse handler. */
private final MouseHandler fMouseHandler= new MouseHandler();
/** The hover. */
private final RevisionHover fHover= new RevisionHover();
/** The annotation listener. */
private final AnnotationListener fAnnotationListener= new AnnotationListener();
/* The context - column and viewer we are connected to. */
/** The vertical ruler column that delegates painting to this painter. */
private final IVerticalRulerColumn fColumn;
/** The parent ruler. */
private CompositeRuler fParentRuler;
/** The column's control, typically a {@link Canvas}, possibly <code>null</code>. */
private Control fControl;
/** The text viewer that the column is attached to. */
private ITextViewer fViewer;
/** The viewer's text widget. */
private StyledText fWidget;
/* The models we operate on. */
/** The revision model object. */
private RevisionInformation fRevisionInfo;
/** The line differ. */
private ILineDiffer fLineDiffer= null;
/** The annotation model. */
private IAnnotationModel fAnnotationModel= null;
/** The background color, possibly <code>null</code>. */
private Color fBackground;
/* Cache. */
/** The cached list of change regions adapted to quick diff. */
private ArrayList fChangeRegions= null;
/** The annotations created for the overview ruler temporary display. */
private List fAnnotations= new ArrayList();
/* State */
/** The current focus line, -1 for none. */
private int fFocusLine= -1;
/** The current focus region, <code>null</code> if none. */
private ChangeRegion fFocusRegion= null;
/** The current focus revision, <code>null</code> if none. */
private Revision fFocusRevision= null;
/** <code>true</code> if the mouse wheel handler is installed, <code>false</code> otherwise. */
private boolean fWheelHandlerInstalled= false;
/**
* <code>true</code> if the overview annotations are displayed, <code>false</code>
* otherwise.
*/
private boolean fIsOverviewShowing= false;
/**
* Creates a new revision painter for a vertical ruler column.
*
* @param column the column that will delegate{@link #paint(GC, ILineRange) painting} to the
* newly created painter.
* @param sharedColors a shared colors object to store shaded colors in
*/
public RevisionPainter(IVerticalRulerColumn column, ISharedTextColors sharedColors) {
Assert.isLegal(column != null);
Assert.isLegal(sharedColors != null);
fColumn= column;
fSharedColors= sharedColors;
}
/**
* Sets the revision information to be drawn and triggers a redraw.
*
* @param info the revision information to show, <code>null</code> to draw none
*/
public void setRevisionInformation(RevisionInformation info) {
fRevisionInfo= info;
fChangeRegions= null;
updateFocusRegion(null);
fColorTool.setInfo(info);
postRedraw();
}
/**
* Sets the background color.
*
* @param background the background color, <code>null</code> for the platform's list
* background
*/
public void setBackground(Color background) {
fBackground= background;
}
/**
* Sets the parent ruler - the delegating column must call this method as soon as it creates its
* control.
*
* @param parentRuler the parent ruler
*/
public void setParentRuler(CompositeRuler parentRuler) {
fParentRuler= parentRuler;
}
/**
* Delegates the painting of the quick diff colors to this painter. The painter will draw the
* color boxes onto the passed {@link GC} for all model (document) lines in
* <code>visibleModelLines</code>.
*
* @param gc the {@link GC} to draw onto
* @param visibleLines the lines (in document offsets) that are currently (perhaps only
* partially) visible
*/
public void paint(GC gc, ILineRange visibleLines) {
connectIfNeeded();
if (!isConnected())
return;
// draw change regions
List/* <ChangeRegion> */changes= getChangeRegions(visibleLines);
for (Iterator it= changes.iterator(); it.hasNext();) {
ChangeRegion region= (ChangeRegion) it.next();
paintChangeRegion(region, gc);
}
}
/**
* Ensures that the column is fully instantiated, i.e. has a control, and that the viewer is
* visible.
*/
private void connectIfNeeded() {
if (isConnected() || fParentRuler == null)
return;
fViewer= fParentRuler.getTextViewer();
if (fViewer == null)
return;
fWidget= fViewer.getTextWidget();
if (fWidget == null)
return;
fControl= fColumn.getControl();
if (fControl == null)
return;
fControl.addMouseTrackListener(fMouseHandler);
fControl.addMouseMoveListener(fMouseHandler);
fControl.addDisposeListener(new DisposeListener() {
/*
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(DisposeEvent e) {
handleDispose();
}
});
}
/**
* Returns <code>true</code> if the column is fully connected.
*
* @return <code>true</code> if the column is fully connected, false otherwise
*/
private boolean isConnected() {
return fControl != null;
}
/**
* Sets the annotation model.
*
* @param model the annotation model, possibly <code>null</code>
* @see IVerticalRulerColumn#setModel(IAnnotationModel)
*/
public void setModel(IAnnotationModel model) {
IAnnotationModel diffModel;
if (model instanceof IAnnotationModelExtension)
diffModel= ((IAnnotationModelExtension) model).getAnnotationModel(IChangeRulerColumn.QUICK_DIFF_MODEL_ID);
else
diffModel= model;
setDiffer(diffModel);
setAnnotationModel(model);
}
/**
* Sets the annotation model.
*
* @param model the annotation model.
*/
private void setAnnotationModel(IAnnotationModel model) {
if (fAnnotationModel != model)
fAnnotationModel= model;
}
/**
* Sets the line differ.
*
* @param differ the line differ
*/
private void setDiffer(IAnnotationModel differ) {
if (differ instanceof ILineDiffer) {
if (fLineDiffer != differ) {
if (fLineDiffer != null)
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= (ILineDiffer) differ;
if (fLineDiffer != null)
((IAnnotationModel) fLineDiffer).addAnnotationModelListener(fAnnotationListener);
redraw();
}
}
}
/**
* Disposes of the painter's resources.
*/
private void handleDispose() {
updateFocusLine(-1);
if (fLineDiffer != null) {
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= null;
}
}
/**
* Paints a single change region onto <code>gc</code>.
*
* @param region the change region to paint
* @param gc the {@link GC} to paint on
*/
private void paintChangeRegion(ChangeRegion region, GC gc) {
Revision revision= region.getRevision();
gc.setBackground(lookupColor(revision, false));
if (revision == fFocusRevision)
gc.setForeground(lookupColor(revision, true));
List ranges= region.getAdjustedRanges();
for (Iterator it= ranges.iterator(); it.hasNext();) {
ILineRange range= (ILineRange) it.next();
Rectangle box= computeBoxBounds(range);
if (box == null)
return;
if (revision == fFocusRevision)
paintHighlight(gc, box);
else
gc.fillRectangle(box);
}
}
/**
* Paints the box for highlighted regions.
*
* @param gc the {@link GC} to draw on
* @param box the box to draw
*/
private void paintHighlight(GC gc, Rectangle box) {
boolean fillGradient= false;
if (fillGradient) {
fillGradientRectangle(gc, box);
} else {
// simple box
gc.fillRectangle(box); // background
gc.drawRectangle(box.x, box.y, box.width - 1, box.height - 1); // highlight box
}
}
/**
* Draws a gradient rectangle inside the box.
*
* @param gc the {@link GC}
* @param box the highlighting box
*/
private void fillGradientRectangle(GC gc, Rectangle box) {
int half= (box.width + 1) / 2;
// left
gc.fillGradientRectangle(box.x, box.y, half, box.height, false);
// right
gc.fillGradientRectangle(box.x + box.width, box.y, -half, box.height, false);
org.eclipse.swt.graphics.Region reg= new org.eclipse.swt.graphics.Region(gc.getDevice());
try {
int[] triangle= { box.x, box.y, box.x + box.width, box.y, box.x + half, box.y + half };
reg.add(triangle);
triangle[1]+= box.height;
triangle[3]+= box.height;
triangle[5]+= box.height - box.width;
reg.add(triangle);
gc.setClipping(reg);
// top
gc.fillGradientRectangle(box.x, box.y, box.width, half, true);
// bottom
gc.fillGradientRectangle(box.x, box.y + box.height, box.width, -half, true);
gc.setClipping((org.eclipse.swt.graphics.Region) null);
} finally {
reg.dispose();
}
}
/**
* Looks up the color for a certain revision.
*
* @param revision the revision to get the color for
* @param focus <code>true</code> if it is the focus revision
* @return the color for the revision
*/
private Color lookupColor(Revision revision, boolean focus) {
return fSharedColors.getColor(fColorTool.getColor(revision, focus));
}
/**
* Returns the change region that contains the given line in one of its adjusted line ranges, or
* <code>null</code> if there is none.
*
* @param line the line of interest
* @return the corresponding <code>ChangeRegion</code> or <code>null</code>
*/
private ChangeRegion getChangeRegion(int line) {
List regions= getRegionCache();
if (regions.isEmpty() || line == -1)
return null;
for (Iterator it= regions.iterator(); it.hasNext();) {
ChangeRegion region= (ChangeRegion) it.next();
if (contains(region.getAdjustedRanges(), line))
return region;
}
// line may be right after the last region
ChangeRegion lastRegion= (ChangeRegion) regions.get(regions.size() - 1);
if (line == end(lastRegion.getAdjustedCoverage()))
return lastRegion;
return null;
}
/**
* Returns the sublist of all <code>ChangeRegion</code>s that intersect with the given lines.
*
* @param lines the model based lines of interest
* @return elementType: ChangeRegion
*/
private List getChangeRegions(ILineRange lines) {
List regions= getRegionCache();
// return the interesting subset
int end= end(lines);
int first= -1, last= -1;
for (int i= 0; i < regions.size(); i++) {
ChangeRegion region= (ChangeRegion) regions.get(i);
int coverageEnd= end(region.getAdjustedCoverage());
if (first == -1 && coverageEnd > lines.getStartLine())
first= i;
if (first != -1 && coverageEnd > end) {
last= i;
break;
}
}
if (first == -1)
return Collections.EMPTY_LIST;
if (last == -1)
last= regions.size() - 1; // bottom index may be one too much
return regions.subList(first, last + 1);
}
/**
* Gets all change regions of the revisions in the revision model and adapts them to the current
* quick diff information. The list is cached.
*
* @return the list of all change regions, with diff information applied
*/
private List getRegionCache() {
if (fChangeRegions == null && fRevisionInfo != null) {
ArrayList regions= new ArrayList();
// flatten
for (Iterator revisions= fRevisionInfo.getRevisions().iterator(); revisions.hasNext();) {
Revision revision= (Revision) revisions.next();
regions.addAll(revision.getRegions());
}
// sort
Collections.sort(regions, new Comparator() {
public int compare(Object o1, Object o2) {
ChangeRegion r1= (ChangeRegion) o1;
ChangeRegion r2= (ChangeRegion) o2;
// sort order is unaffected by diff information
return r1.getOriginalRange().getStartLine() - r2.getOriginalRange().getStartLine();
}
});
if (fLineDiffer != null)
new DiffApplier().applyDiff(regions, fLineDiffer, fViewer.getDocument().getNumberOfLines());
fChangeRegions= regions;
}
if (fChangeRegions == null)
return Collections.EMPTY_LIST;
return fChangeRegions;
}
/**
* Returns <code>true</code> if <code>range</code> contains <code>line</code>. A line is
* not contained in a range if it is the range's exclusive end line.
*
* @param range the range to check whether it contains <code>line</code>
* @param line the line the line
* @return <code>true</code> if <code>range</code> contains <code>line</code>,
* <code>false</code> if not
*/
private static boolean contains(ILineRange range, int line) {
return range.getStartLine() <= line && end(range) > line;
}
/**
* Returns <code>true</code> if any of the line ranges
* {@link #contains(ILineRange, int) contains} <code>line</code>.
*
* @param ranges a list of {@link ILineRange}s
* @param line a line
* @return <code>true</code> if <code>line</code> is contained in any of the passed
* <code>ranges</code>
*/
private static boolean contains(List ranges, int line) {
for (Iterator it= ranges.iterator(); it.hasNext();) {
ILineRange range= (ILineRange) it.next();
if (contains(range, line))
return true;
}
return false;
}
/**
* Computes the end index of a line range.
*
* @param range a line range
* @return the last line (exclusive) of <code>range</code>
*/
private static int end(ILineRange range) {
return range.getStartLine() + range.getNumberOfLines();
}
/**
* Returns the visible extent of a document line range in widget lines.
*
* @param range the document line range
* @return the visible extent of <code>range</code> in widget lines
*/
private ILineRange modelLinesToWidgetLines(ILineRange range) {
int widgetStartLine= -1;
int widgetEndLine= -1;
if (fViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension= (ITextViewerExtension5) fViewer;
int modelEndLine= end(range);
for (int modelLine= range.getStartLine(); modelLine < modelEndLine; modelLine++) {
int widgetLine= extension.modelLine2WidgetLine(modelLine);
if (widgetLine != -1) {
if (widgetStartLine == -1)
widgetStartLine= widgetLine;
widgetEndLine= widgetLine;
}
}
} else {
IRegion region= fViewer.getVisibleRegion();
IDocument document= fViewer.getDocument();
try {
int visibleStartLine= document.getLineOfOffset(region.getOffset());
int visibleEndLine= document.getLineOfOffset(region.getOffset() + region.getLength());
widgetStartLine= Math.max(0, range.getStartLine() - visibleStartLine);
widgetEndLine= Math.min(visibleEndLine, end(range) - 1);
} catch (BadLocationException x) {
x.printStackTrace();
// ignore and return null
}
}
if (widgetStartLine == -1 || widgetEndLine == -1)
return null;
return new LineRange(widgetStartLine, widgetEndLine - widgetStartLine + 1);
}
/**
* Returns the revision hover.
*
* @return the revision hover
*/
public IAnnotationHover getHover() {
return fHover;
}
/**
* Computes and returns the bounds of the rectangle corresponding to a document line range. The
* rectangle is in pixel coordinates relative to the text widget's
* {@link StyledText#getClientArea() client area} and has the width of the ruler.
*
* @param range the document line range
* @return the box bounds corresponding to <code>range</code>
*/
private Rectangle computeBoxBounds(ILineRange range) {
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (widgetRange == null)
return null;
int y1= fWidget.getLinePixel(widgetRange.getStartLine());
int y2= fWidget.getLinePixel(widgetRange.getStartLine() + widgetRange.getNumberOfLines());
return new Rectangle(0, y1, getWidth(), y2 - y1 - 1);
}
/**
* Shows (or hides) the overview annotations. Pass <code>null</code> to remove any displayed
* annotations.
*
* @param revision the revision to show in the overview ruler
*/
private void showOverviewAnnotations(Revision revision) {
if (fAnnotationModel == null)
return;
Map added= null;
if (revision != null && fIsOverviewShowing) {
added= new HashMap();
for (Iterator it= revision.getRegions().iterator(); it.hasNext();) {
ChangeRegion region= (ChangeRegion) it.next();
for (Iterator regions= region.getAdjustedRanges().iterator(); regions.hasNext();) {
ILineRange range= (ILineRange) regions.next();
try {
IRegion charRegion= toCharRegion(range);
Position position= new Position(charRegion.getOffset(), charRegion.getLength());
Annotation annotation= new RevisionAnnotation(revision.getId());
added.put(annotation, position);
} catch (BadLocationException x) {
// ignore - document was changed, show no annotations
}
}
}
}
if (fAnnotationModel instanceof IAnnotationModelExtension) {
IAnnotationModelExtension ext= (IAnnotationModelExtension) fAnnotationModel;
ext.replaceAnnotations((Annotation[]) fAnnotations.toArray(new Annotation[fAnnotations.size()]), added);
} else {
for (Iterator it= fAnnotations.iterator(); it.hasNext();) {
Annotation annotation= (Annotation) it.next();
fAnnotationModel.removeAnnotation(annotation);
}
if (added != null) {
for (Iterator it= added.entrySet().iterator(); it.hasNext();) {
Entry entry= (Entry) it.next();
fAnnotationModel.addAnnotation((Annotation) entry.getKey(), (Position) entry.getValue());
}
}
}
fAnnotations.clear();
if (added != null)
fAnnotations.addAll(added.keySet());
}
/**
* Returns the character offset based region of a line range.
*
* @param lines the line range to convert
* @return the character offset range corresponding to <code>range</code>
* @throws BadLocationException if the line range is not within the document bounds
*/
private IRegion toCharRegion(ILineRange lines) throws BadLocationException {
IDocument document= fViewer.getDocument();
int offset= document.getLineOffset(lines.getStartLine());
int nextLine= end(lines);
int endOffset;
if (nextLine >= document.getNumberOfLines())
endOffset= document.getLength();
else
endOffset= document.getLineOffset(nextLine);
return new Region(offset, endOffset - offset);
}
/**
* Updates the focus line with a new line.
*
* @param line the new focus line, -1 for no focus
*/
private void updateFocusLine(int line) {
if (fFocusLine != line)
onFocusLineChanged(fFocusLine, line);
}
/**
* Handles a changing focus line.
*
* @param previousLine the old focus line (-1 for no focus)
* @param nextLine the new focus line (-1 for no focus)
*/
private void onFocusLineChanged(int previousLine, int nextLine) {
if (DEBUG)
System.out.println("line: " + previousLine + " > " + nextLine); //$NON-NLS-1$ //$NON-NLS-2$
fFocusLine= nextLine;
ChangeRegion region= getChangeRegion(nextLine);
updateFocusRegion(region);
}
/**
* Updates the focus region.
*
* @param region the new focus region, <code>null</code> for no focus
*/
private void updateFocusRegion(ChangeRegion region) {
if (region != fFocusRegion)
onFocusRegionChanged(fFocusRegion, region);
}
/**
* Handles a changing focus region.
*
* @param previousRegion the old focus region (<code>null</code> for no focus)
* @param nextRegion the new focus region (<code>null</code> for no focus)
*/
private void onFocusRegionChanged(ChangeRegion previousRegion, ChangeRegion nextRegion) {
if (DEBUG)
System.out.println("region: " + previousRegion + " > " + nextRegion); //$NON-NLS-1$ //$NON-NLS-2$
fFocusRegion= nextRegion;
Revision revision= nextRegion == null ? null : nextRegion.getRevision();
if (fFocusRevision != revision)
onFocusRevisionChanged(fFocusRevision, revision);
}
/**
* Handles a changing focus revision.
*
* @param previousRevision the old focus revision (<code>null</code> for no focus)
* @param nextRevision the new focus revision (<code>null</code> for no focus)
*/
private void onFocusRevisionChanged(Revision previousRevision, Revision nextRevision) {
if (DEBUG)
System.out.println("revision: " + previousRevision + " > " + nextRevision); //$NON-NLS-1$ //$NON-NLS-2$
fFocusRevision= nextRevision;
uninstallWheelHandler();
showOverviewAnnotations(fFocusRevision);
redraw(); // pick up new highlights
}
/**
* Uninstalls the mouse wheel handler.
*/
private void uninstallWheelHandler() {
fControl.removeListener(SWT.MouseWheel, fMouseHandler);
fWheelHandlerInstalled= false;
}
/**
* Installs the mouse wheel handler.
*/
private void installWheelHandler() {
if (fFocusRevision != null && !fWheelHandlerInstalled) {
fControl.addListener(SWT.MouseWheel, fMouseHandler);
fWheelHandlerInstalled= true;
}
}
/**
* Handles a hover event on the focus revision.
*/
private void onHover() {
installWheelHandler();
}
/**
* Handles a mouse enter event on the focus revision
*/
private void onEnter() {
fIsOverviewShowing= true;
}
/**
* Handles a mouse exit event on the focus revision
*/
private void onExit() {
fIsOverviewShowing= false;
}
/**
* Handles a mouse wheel event.
*
* @param event the mouse wheel event
*/
private void handleMouseWheel(Event event) {
boolean up= event.count > 0;
int documentHoverLine= fFocusLine;
ILineRange nextWidgetRange= null;
ILineRange last= null;
if (up) {
outer: for (Iterator it= fFocusRevision.getRegions().iterator(); it.hasNext();) {
ChangeRegion region= (ChangeRegion) it.next();
for (Iterator regions= region.getAdjustedRanges().iterator(); regions.hasNext();) {
ILineRange range= (ILineRange) regions.next();
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (contains(range, documentHoverLine)) {
nextWidgetRange= last;
break outer;
}
if (widgetRange != null)
last= widgetRange;
}
}
} else {
outer: for (ListIterator it= fFocusRevision.getRegions().listIterator(fFocusRevision.getRegions().size()); it.hasPrevious();) {
ChangeRegion region= (ChangeRegion) it.previous();
for (ListIterator regions= region.getAdjustedRanges().listIterator(region.getAdjustedRanges().size()); regions.hasPrevious();) {
ILineRange range= (ILineRange) regions.previous();
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (contains(range, documentHoverLine)) {
nextWidgetRange= last;
break outer;
}
if (widgetRange != null)
last= widgetRange;
}
}
}
if (nextWidgetRange == null)
return;
int widgetCurrentFocusLine= modelLinesToWidgetLines(new LineRange(documentHoverLine, 1)).getStartLine();
int widgetNextFocusLine= nextWidgetRange.getStartLine();
int newTopPixel= fWidget.getTopPixel() + JFaceTextUtil.computeLineHeight(fWidget, widgetCurrentFocusLine, widgetNextFocusLine, widgetNextFocusLine - widgetCurrentFocusLine);
fWidget.setTopPixel(newTopPixel);
if (newTopPixel < 0) {
Point cursorLocation= fWidget.getDisplay().getCursorLocation();
cursorLocation.y+= newTopPixel;
fWidget.getDisplay().setCursorLocation(cursorLocation);
} else {
int topPixel= fWidget.getTopPixel();
if (topPixel < newTopPixel) {
Point cursorLocation= fWidget.getDisplay().getCursorLocation();
cursorLocation.y+= newTopPixel - topPixel;
fWidget.getDisplay().setCursorLocation(cursorLocation);
}
}
updateFocusLine(toDocumentLineNumber(fWidget.toControl(fWidget.getDisplay().getCursorLocation()).y));
immediateUpdate();
}
/**
* Triggers a redraw in the display thread.
*/
private final void postRedraw() {
if (isConnected() && !fControl.isDisposed()) {
Display d= fControl.getDisplay();
if (d != null) {
d.asyncExec(new Runnable() {
public void run() {
redraw();
}
});
}
}
}
/**
* Translates a y coordinate in the pixel coordinates of the column's control to a document line
* number.
*
* @param y the y coordinate
* @return the corresponding document line, -1 for no line
* @see CompositeRuler#toDocumentLineNumber(int)
*/
private int toDocumentLineNumber(int y) {
return fParentRuler.toDocumentLineNumber(y);
}
/**
* Triggers redrawing of the column.
*/
private void redraw() {
fColumn.redraw();
}
/**
* Triggers immediate redrawing of the entire column - use with care.
*/
private void immediateUpdate() {
fParentRuler.immediateUpdate();
}
/**
* Returns the width of the column.
*
* @return the width of the column
*/
private int getWidth() {
return fColumn.getWidth();
}
/**
* Returns the System background color for list widgets.
*
* @return the System background color for list widgets
*/
private Color getBackground() {
if (fBackground == null)
return fWidget.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND);
return fBackground;
}
/**
* Sets the hover later returned by {@link #getHover()}.
*
* @param hover the hover
*/
public void setHover(IAnnotationHover hover) {
// TODO ignore for now - must make revision hover settable from outside
}
/**
* Returns <code>true</code> if the receiver can provide a hover for a certain document line.
*
* @param activeLine the document line of interest
* @return <code>true</code> if the receiver can provide a hover
*/
public boolean hasHover(int activeLine) {
return fViewer instanceof ISourceViewer && fHover.getHoverLineRange((ISourceViewer) fViewer, activeLine) != null;
}
}