blob: 0daf9b69e007f1a2767d729d4ad64fb97e38b557 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.text.revisions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
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.FontMetrics;
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.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.internal.text.html.BrowserInformationControl;
import org.eclipse.jface.internal.text.html.HTMLPrinter;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
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.JFaceTextUtil;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.information.IInformationProviderExtension2;
import org.eclipse.jface.text.revisions.IRevisionListener;
import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension;
import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension.RenderingMode;
import org.eclipse.jface.text.revisions.Revision;
import org.eclipse.jface.text.revisions.RevisionEvent;
import org.eclipse.jface.text.revisions.RevisionInformation;
import org.eclipse.jface.text.revisions.RevisionRange;
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;
/**
* 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$
// RGBs provided by UI Designer
private static final RGB BY_DATE_START_COLOR= new RGB(199, 134, 57);
private static final RGB BY_DATE_END_COLOR= new RGB(241, 225, 206);
/**
* 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.5f;
/**
* The maximum shading in [0, 1] - this is the shade that the most recent revision will
* receive.
*/
private static final float MAX_SHADING= 0.7f;
/**
* The minimum shading in [0, 1] - this is the shade that the oldest revision will receive.
*/
private static final float MIN_SHADING= 0.2f;
/**
* The shade for the focus boxes.
*/
private static final float FOCUS_COLOR_SHADING= 1f;
/**
* A list of {@link Long}, storing the age of each revision in a sorted list.
*/
private List<Long> fRevisions;
/**
* The stored shaded colors.
*/
private final Map<Revision, RGB> fColors= new HashMap<>();
/**
* The stored focus colors.
*/
private final Map<Revision, RGB> 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<Long> revisions= new ArrayList<>();
for (Revision revision : info.getRevisions()) {
revisions.add(Long.valueOf(computeAge(revision)));
}
Collections.sort(revisions);
fRevisions= revisions;
}
private RGB adaptColor(Revision revision, boolean focus) {
RGB rgb;
float scale;
if (fRenderingMode == IRevisionRulerColumnExtension.AGE) {
int index= computeAgeIndex(revision);
if (index == -1 || fRevisions.size() == 0) {
rgb= getBackground().getRGB();
} else {
// gradient from intense red for most recent to faint yellow for oldest
RGB[] gradient= Colors.palette(BY_DATE_START_COLOR, BY_DATE_END_COLOR, fRevisions.size());
rgb= gradient[gradient.length - index - 1];
}
scale= 0.99f;
} else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR) {
rgb= revision.getColor();
rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY);
scale= 0.6f;
} else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR_SHADED_BY_AGE) {
rgb= revision.getColor();
rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY);
int index= computeAgeIndex(revision);
int size= fRevisions.size();
// relative age: newest is 0, oldest is 1
// if there is only one revision, use an intermediate value to avoid extreme coloring
if (index == -1 || size < 2)
scale= 0.5f;
else
scale= (float) index / (size - 1);
} else {
Assert.isTrue(false);
return null; // dummy
}
rgb= getShadedColor(rgb, scale, focus);
return rgb;
}
private int computeAgeIndex(Revision revision) {
long age= computeAge(revision);
int index= fRevisions.indexOf(Long.valueOf(age));
return index;
}
private RGB getShadedColor(RGB color, float scale, boolean focus) {
Assert.isLegal(scale >= 0.0);
Assert.isLegal(scale <= 1.0);
RGB background= getBackground().getRGB();
// normalize to lie within [MIN_SHADING, MAX_SHADING]
// use more intense colors if the ruler is narrow (i.e. not showing line numbers)
boolean makeIntense= getWidth() <= 15;
float intensityShift= makeIntense ? 0.3f : 0f;
float max= MAX_SHADING + intensityShift;
float min= MIN_SHADING + intensityShift;
scale= (max - min) * scale + min;
// focus coloring
if (focus) {
scale += FOCUS_COLOR_SHADING;
if (scale > 1) {
background= new RGB(255 - background.red, 255 - background.green, 255 - background.blue);
scale= 2 - scale;
}
}
return Colors.blend(background, color, scale);
}
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<Revision, RGB> map= focus ? fFocusColors : fColors;
RGB color= map.get(revision);
if (color != null)
return color;
color= adaptColor(revision, 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 {
private RevisionRange fMouseDownRegion;
private void handleMouseUp(Event e) {
if (e.button == 1) {
RevisionRange upRegion= fFocusRange;
RevisionRange downRegion= fMouseDownRegion;
fMouseDownRegion= null;
if (upRegion == downRegion) {
Revision revision= upRegion == null ? null : upRegion.getRevision();
if (revision == fSelectedRevision)
revision= null; // deselect already selected revision
handleRevisionSelected(revision);
}
}
}
private void handleMouseDown(Event e) {
if (e.button == 3)
updateFocusRevision(null); // kill any focus as the ctx menu is going to show
if (e.button == 1) {
fMouseDownRegion= fFocusRange;
postRedraw();
}
}
@Override
public void handleEvent(Event event) {
switch (event.type) {
case SWT.MouseVerticalWheel:
handleMouseWheel(event);
break;
case SWT.MouseDown:
handleMouseDown(event);
break;
case SWT.MouseUp:
handleMouseUp(event);
break;
default:
Assert.isLegal(false);
}
}
@Override
public void mouseEnter(MouseEvent e) {
updateFocusLine(toDocumentLineNumber(e.y));
}
@Override
public void mouseExit(MouseEvent e) {
updateFocusLine(-1);
}
@Override
public void mouseHover(MouseEvent e) {
}
@Override
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 {
@Override
public void modelChanged(IAnnotationModel model) {
clearRangeCache();
postRedraw();
}
}
/**
* The information control creator.
*/
private static final class HoverInformationControlCreator extends AbstractReusableInformationControlCreator {
private boolean fIsFocusable;
public HoverInformationControlCreator(boolean isFocusable) {
fIsFocusable= isFocusable;
}
@Override
protected IInformationControl doCreateInformationControl(Shell parent) {
if (BrowserInformationControl.isAvailable(parent)) {
return new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, fIsFocusable) {
/**
* {@inheritDoc}
*
* @deprecated use {@link #setInput(Object)}
*/
@Deprecated
@Override
public void setInformation(String content) {
content= addCSSToHTMLFragment(content);
super.setInformation(content);
}
/**
* 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 String 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;
StringBuilder info= new StringBuilder(512 + html.length());
HTMLPrinter.insertPageProlog(info, 0, fgStyleSheet);
info.append(html);
HTMLPrinter.addPageEpilog(info);
return info.toString();
}
};
}
return new DefaultInformationControl(parent, fIsFocusable);
}
@Override
public boolean canReplace(IInformationControlCreator creator) {
return creator.getClass() == getClass()
&& ((HoverInformationControlCreator) creator).fIsFocusable == fIsFocusable;
}
}
private static final String fgStyleSheet= "/* Font definitions */\n" + //$NON-NLS-1$
"body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt {font-family: sans-serif; font-size: 9pt }\n" + //$NON-NLS-1$
"pre { font-family: monospace; font-size: 9pt }\n" + //$NON-NLS-1$
"\n" + //$NON-NLS-1$
"/* Margins */\n" + //$NON-NLS-1$
"body { overflow: auto; margin-top: 0; margin-bottom: 4; margin-left: 3; margin-right: 0 }\n" + //$NON-NLS-1$
"h1 { margin-top: 5; margin-bottom: 1 } \n" + //$NON-NLS-1$
"h2 { margin-top: 25; margin-bottom: 3 }\n" + //$NON-NLS-1$
"h3 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$
"h4 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$
"h5 { 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$
"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$
"\n" + //$NON-NLS-1$
"/* Styles and colors */\n" + //$NON-NLS-1$
"a:link { color: hyperlinkColor }\n" + //$NON-NLS-1$
"a:hover { color: activeHyperlinkColor; }\n" + //$NON-NLS-1$
"a:visited { text-decoration: underline }\n" + //$NON-NLS-1$
"h4 { font-style: italic }\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$
"th { font-weight: bold }\n" + //$NON-NLS-1$
""; //$NON-NLS-1$
/**
* The revision hover displays information about the currently selected revision.
*/
private final class RevisionHover implements IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2, IInformationProviderExtension2 {
@Override
public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) {
Object info= getHoverInfo(sourceViewer, getHoverLineRange(sourceViewer, lineNumber), 0);
return info == null ? null : info.toString();
}
@Override
public IInformationControlCreator getHoverControlCreator() {
RevisionInformation revisionInfo= fRevisionInfo;
if (revisionInfo != null) {
IInformationControlCreator creator= revisionInfo.getHoverControlCreator();
if (creator != null)
return creator;
}
return new HoverInformationControlCreator(false);
}
@Override
public boolean canHandleMouseCursor() {
return false;
}
@Override
public boolean canHandleMouseWheel() {
return true;
}
@Override
public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) {
RevisionRange range= getRange(lineRange.getStartLine());
Object info= range == null ? null : range.getRevision().getHoverInfo();
return info;
}
@Override
public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) {
RevisionRange range= getRange(lineNumber);
return range == null ? null : new LineRange(lineNumber, 1);
}
@Override
public IInformationControlCreator getInformationPresenterControlCreator() {
RevisionInformation revisionInfo= fRevisionInfo;
if (revisionInfo != null) {
IInformationControlCreator creator= revisionInfo.getInformationPresenterControlCreator();
if (creator != null)
return creator;
}
return new HoverInformationControlCreator(true);
}
}
/* 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 selection provider. */
private final RevisionSelectionProvider fRevisionSelectionProvider= new RevisionSelectionProvider(this);
/**
* The list of revision listeners.
* @since 3.3.
*/
private final ListenerList<IRevisionListener> fRevisionListeners= new ListenerList<>(ListenerList.IDENTITY);
/* 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 ranges adapted to quick diff. */
private List<RevisionRange> fRevisionRanges= null;
/** The annotations created for the overview ruler temporary display. */
private List<Annotation> 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 RevisionRange fFocusRange= null;
/** The current focus revision, <code>null</code> if none. */
private Revision fFocusRevision= null;
/**
* The currently selected revision, <code>null</code> if none. The difference between
* {@link #fFocusRevision} and {@link #fSelectedRevision} may not be obvious: the focus revision
* is the one focused by the mouse (by hovering over a block of the revision), while the
* selected revision is sticky, i.e. is not removed when the mouse leaves the ruler.
*
* @since 3.3
*/
private Revision fSelectedRevision= null;
/** <code>true</code> if the mouse wheel handler is installed, <code>false</code> otherwise. */
private boolean fWheelHandlerInstalled= false;
/**
* The revision rendering mode.
*/
private RenderingMode fRenderingMode= IRevisionRulerColumnExtension.AUTHOR_SHADED_BY_AGE;
/**
* The required with in characters.
* @since 3.3
*/
private int fRequiredWidth= -1;
/**
* The width of the revision field in chars to compute {@link #fAuthorInset} from.
* @since 3.3
*/
private int fRevisionIdChars= 0;
/**
* <code>true</code> to show revision ids, <code>false</code> otherwise.
* @since 3.3
*/
private boolean fShowRevision= false;
/**
* <code>true</code> to show the author, <code>false</code> otherwise.
* @since 3.3
*/
private boolean fShowAuthor= false;
/**
* The author inset in pixels for when author *and* revision id are shown.
* @since 3.3
*/
private int fAuthorInset;
/**
* The remembered ruler width (as changing the ruler width triggers recomputation of the colors.
* @since 3.3
*/
private int fLastWidth= -1;
/**
* The zoom level for the current painting operation. Workaround for bug 516293.
* @since 3.12
*/
private int fZoom= 100;
/**
* 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) {
if (fRevisionInfo != info) {
fRequiredWidth= -1;
fRevisionIdChars= 0;
fRevisionInfo= info;
clearRangeCache();
updateFocusRange(null);
handleRevisionSelected((Revision) null);
fColorTool.setInfo(info);
postRedraw();
informListeners();
}
}
/**
* Changes the rendering mode and triggers redrawing if needed.
*
* @param renderingMode the rendering mode
* @since 3.3
*/
public void setRenderingMode(RenderingMode renderingMode) {
Assert.isLegal(renderingMode != null);
if (fRenderingMode != renderingMode) {
fRenderingMode= renderingMode;
fColorTool.setInfo(fRevisionInfo);
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;
}
/**
* Sets the zoom level for the current painting operation. Workaround for bug 516293.
*
* @param zoom the zoom to set
* @since 3.12
*/
public void setZoom(int zoom) {
fZoom= zoom;
}
private int autoScaleUp(int value) {
return value * fZoom / 100;
}
/**
* 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;
// compute the horizontal indent of the author for the case that we show revision
// and author
if (fShowAuthor && fShowRevision) {
char[] string= new char[fRevisionIdChars + 1];
Arrays.fill(string, '9');
if (string.length > 1) {
string[0]= '.';
string[1]= ' ';
}
fAuthorInset= gc.stringExtent(new String(string)).x;
}
// recompute colors (show intense colors if ruler is narrow)
int width= getWidth();
if (width != fLastWidth) {
fColorTool.setInfo(fRevisionInfo);
fLastWidth= width;
}
// draw change regions
List<RevisionRange> ranges= getRanges(visibleLines);
for (RevisionRange region : ranges) {
paintRange(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.addListener(SWT.MouseUp, fMouseHandler);
fControl.addListener(SWT.MouseDown, fMouseHandler);
fControl.addDisposeListener(e -> handleDispose());
fRevisionSelectionProvider.install(fViewer);
}
/**
* 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 or <code>null</code> if none
*/
private void setDiffer(IAnnotationModel differ) {
if (differ instanceof ILineDiffer || differ == null) {
if (fLineDiffer != differ) {
if (fLineDiffer != null)
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= (ILineDiffer) differ;
if (fLineDiffer != null)
((IAnnotationModel) fLineDiffer).addAnnotationModelListener(fAnnotationListener);
}
}
}
/**
* Disposes of the painter's resources.
*/
private void handleDispose() {
updateFocusLine(-1);
if (fLineDiffer != null) {
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= null;
}
fRevisionSelectionProvider.uninstall();
}
/**
* Paints a single change region onto <code>gc</code>.
*
* @param range the range to paint
* @param gc the {@link GC} to paint on
*/
private void paintRange(RevisionRange range, GC gc) {
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (widgetRange == null)
return;
Revision revision= range.getRevision();
boolean drawArmedFocus= range == fMouseHandler.fMouseDownRegion;
boolean drawSelection= !drawArmedFocus && revision == fSelectedRevision;
boolean drawFocus= !drawSelection && !drawArmedFocus && revision == fFocusRevision;
Rectangle box= computeBoxBounds(widgetRange);
gc.setBackground(lookupColor(revision, false));
if (drawArmedFocus) {
Color foreground= gc.getForeground();
Color focusColor= lookupColor(revision, true);
gc.setForeground(focusColor);
gc.fillRectangle(box);
gc.drawRectangle(box.x, box.y, box.width - 1, box.height - 1); // highlight box
gc.drawRectangle(box.x + 1, box.y + 1, box.width - 3, box.height - 3); // inner highlight box
gc.setForeground(foreground);
} else if (drawFocus || drawSelection) {
Color foreground= gc.getForeground();
Color focusColor= lookupColor(revision, true);
gc.setForeground(focusColor);
gc.fillRectangle(box);
gc.drawRectangle(box.x, box.y, box.width - 1, box.height - 1); // highlight box
gc.setForeground(foreground);
} else {
gc.fillRectangle(box);
}
if ((fShowAuthor || fShowRevision)) {
int indentation= 1;
int baselineBias= getBaselineBias(gc, widgetRange.getStartLine());
if (fShowAuthor && fShowRevision) {
gc.drawString(revision.getId(), indentation, box.y + baselineBias, true);
gc.drawString(revision.getAuthor(), fAuthorInset, box.y + baselineBias, true);
} else if (fShowAuthor) {
gc.drawString(revision.getAuthor(), indentation, box.y + baselineBias, true);
} else if (fShowRevision) {
gc.drawString(revision.getId(), indentation, box.y + baselineBias, true);
}
}
}
/**
* Returns the difference between the baseline of the widget and the
* baseline as specified by the font for <code>gc</code>. When drawing
* line numbers, the returned bias should be added to obtain text lined up
* on the correct base line of the text widget.
*
* @param gc the <code>GC</code> to get the font metrics from
* @param widgetLine the widget line
* @return the baseline bias to use when drawing text that is lined up with
* <code>fCachedTextWidget</code>
* @since 3.3
*/
private int getBaselineBias(GC gc, int widgetLine) {
if (widgetLine == fWidget.getLineCount())
widgetLine--;
/*
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=62951
* widget line height may be more than the font height used for the
* line numbers, since font styles (bold, italics...) can have larger
* font metrics than the simple font used for the numbers.
*/
int offset= fWidget.getOffsetAtLine(widgetLine);
int widgetBaseline= fWidget.getBaseline(offset);
FontMetrics fm = gc.getFontMetrics();
int fontBaseline = fm.getAscent() + fm.getLeading();
int baselineBias= widgetBaseline - fontBaseline;
return Math.max(0, baselineBias);
}
/**
* 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 revision range that contains the given line, or
* <code>null</code> if there is none.
*
* @param line the line of interest
* @return the corresponding <code>RevisionRange</code> or <code>null</code>
*/
private RevisionRange getRange(int line) {
List<RevisionRange> ranges= getRangeCache();
if (ranges.isEmpty() || line == -1)
return null;
for (RevisionRange range : ranges) {
if (contains(range, line))
return range;
}
// line may be right after the last region
RevisionRange lastRegion= ranges.get(ranges.size() - 1);
if (line == end(lastRegion))
return lastRegion;
return null;
}
/**
* Returns the sublist of all <code>RevisionRange</code>s that intersect with the given lines.
*
* @param lines the model based lines of interest
* @return elementType: RevisionRange
*/
private List<RevisionRange> getRanges(ILineRange lines) {
List<RevisionRange> ranges= getRangeCache();
// return the interesting subset
int end= end(lines);
int first= -1, last= -1;
for (int i= 0; i < ranges.size(); i++) {
RevisionRange range= ranges.get(i);
int rangeEnd= end(range);
if (first == -1 && rangeEnd > lines.getStartLine())
first= i;
if (first != -1 && rangeEnd > end) {
last= i;
break;
}
}
if (first == -1)
return Collections.emptyList();
if (last == -1)
last= ranges.size() - 1; // bottom index may be one too much
return ranges.subList(first, last + 1);
}
/**
* Gets all change ranges 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 synchronized List<RevisionRange> getRangeCache() {
if (fRevisionRanges == null) {
if (fRevisionInfo == null) {
fRevisionRanges= Collections.emptyList();
} else {
Hunk[] hunks= HunkComputer.computeHunks(fLineDiffer, fViewer.getDocument().getNumberOfLines());
fRevisionInfo.applyDiff(hunks);
fRevisionRanges= fRevisionInfo.getRanges();
updateOverviewAnnotations();
informListeners();
}
}
return fRevisionRanges;
}
/**
* Clears the range cache.
*
* @since 3.3
*/
private synchronized void clearRangeCache() {
fRevisionRanges= null;
}
/**
* 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;
}
/**
* 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) {
// 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 widget 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 widget line range
* @return the box bounds corresponding to <code>range</code>
*/
private Rectangle computeBoxBounds(ILineRange range) {
int y1= fWidget.getLinePixel(range.getStartLine());
int y2= fWidget.getLinePixel(range.getStartLine() + range.getNumberOfLines());
return new Rectangle(0, autoScaleUp(y1), autoScaleUp(getWidth()), autoScaleUp(y2 - y1 - 1));
}
/**
* Shows (or hides) the overview annotations.
*/
private void updateOverviewAnnotations() {
if (fAnnotationModel == null)
return;
Revision revision= fFocusRevision != null ? fFocusRevision : fSelectedRevision;
Map<Annotation, Position> added= null;
if (revision != null) {
added= new HashMap<>();
for (RevisionRange range : revision.getRegions()) {
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(fAnnotations.toArray(new Annotation[fAnnotations.size()]), added);
} else {
for (Annotation annotation : fAnnotations) {
fAnnotationModel.removeAnnotation(annotation);
}
if (added != null) {
for (Entry<Annotation, Position> entry : added.entrySet()) {
fAnnotationModel.addAnnotation(entry.getKey(), 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);
}
/**
* Handles the selection of a revision and informs listeners.
*
* @param revision the selected revision, <code>null</code> for none
*/
void handleRevisionSelected(Revision revision) {
fSelectedRevision= revision;
fRevisionSelectionProvider.revisionSelected(revision);
if (isConnected())
updateOverviewAnnotations();
postRedraw();
}
/**
* Handles the selection of a revision id and informs listeners
*
* @param id the selected revision id
*/
void handleRevisionSelected(String id) {
Assert.isLegal(id != null);
if (fRevisionInfo == null)
return;
for (Revision revision : fRevisionInfo.getRevisions()) {
if (id.equals(revision.getId())) {
handleRevisionSelected(revision);
return;
}
}
// clear selection if it does not exist
handleRevisionSelected((Revision) null);
}
/**
* Returns the selection provider.
*
* @return the selection provider
*/
public RevisionSelectionProvider getRevisionSelectionProvider() {
return fRevisionSelectionProvider;
}
/**
* 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;
RevisionRange region= getRange(nextLine);
updateFocusRange(region);
}
/**
* Updates the focus range.
*
* @param range the new focus range, <code>null</code> for no focus
*/
private void updateFocusRange(RevisionRange range) {
if (range != fFocusRange)
onFocusRangeChanged(fFocusRange, range);
}
/**
* Handles a changing focus range.
*
* @param previousRange the old focus range (<code>null</code> for no focus)
* @param nextRange the new focus range (<code>null</code> for no focus)
*/
private void onFocusRangeChanged(RevisionRange previousRange, RevisionRange nextRange) {
if (DEBUG)
System.out.println("range: " + previousRange + " > " + nextRange); //$NON-NLS-1$ //$NON-NLS-2$
fFocusRange= nextRange;
Revision revision= nextRange == null ? null : nextRange.getRevision();
updateFocusRevision(revision);
}
private void updateFocusRevision(Revision revision) {
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();
installWheelHandler();
updateOverviewAnnotations();
redraw(); // pick up new highlights
}
/**
* Uninstalls the mouse wheel handler.
*/
private void uninstallWheelHandler() {
fControl.removeListener(SWT.MouseVerticalWheel, fMouseHandler);
fWheelHandlerInstalled= false;
}
/**
* Installs the mouse wheel handler.
*/
private void installWheelHandler() {
if (fFocusRevision != null && !fWheelHandlerInstalled) {
//FIXME: does not work on Windows, because Canvas cannot get focus and therefore does not send out mouse wheel events:
//https://bugs.eclipse.org/bugs/show_bug.cgi?id=81189
//see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=75766
fControl.addListener(SWT.MouseVerticalWheel, fMouseHandler);
fWheelHandlerInstalled= true;
}
}
/**
* @return <code>true</code> iff the mouse wheel handler is installed and others should avoid
* handling mouse wheel events
* @since 3.10
*/
public boolean isWheelHandlerInstalled() {
return fWheelHandlerInstalled;
}
/**
* 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;
List<RevisionRange> ranges= fFocusRevision.getRegions();
if (up) {
for (RevisionRange range : ranges) {
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (contains(range, documentHoverLine)) {
nextWidgetRange= last;
break;
}
if (widgetRange != null)
last= widgetRange;
}
} else {
for (ListIterator<RevisionRange> it= ranges.listIterator(ranges.size()); it.hasPrevious();) {
RevisionRange range= it.previous();
ILineRange widgetRange= modelLinesToWidgetLines(range);
if (contains(range, documentHoverLine)) {
nextWidgetRange= last;
break;
}
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(() -> 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;
}
/**
* Returns the revision at a certain document offset, or <code>null</code> for none.
*
* @param offset the document offset
* @return the revision at offset, or <code>null</code> for none
*/
Revision getRevision(int offset) {
IDocument document= fViewer.getDocument();
int line;
try {
line= document.getLineOfOffset(offset);
} catch (BadLocationException x) {
return null;
}
if (line != -1) {
RevisionRange range= getRange(line);
if (range != null)
return range.getRevision();
}
return null;
}
/**
* Returns <code>true</code> if a revision model has been set, <code>false</code> otherwise.
*
* @return <code>true</code> if a revision model has been set, <code>false</code> otherwise
*/
public boolean hasInformation() {
return fRevisionInfo != null;
}
/**
* Returns the width in chars required to display information.
*
* @return the width in chars required to display information
* @since 3.3
*/
public int getRequiredWidth() {
if (fRequiredWidth == -1) {
if (hasInformation() && (fShowRevision || fShowAuthor)) {
int revisionWidth= 0;
int authorWidth= 0;
for (Revision revision : fRevisionInfo.getRevisions()) {
revisionWidth= Math.max(revisionWidth, revision.getId().length());
authorWidth= Math.max(authorWidth, revision.getAuthor().length());
}
fRevisionIdChars= revisionWidth + 1;
if (fShowAuthor && fShowRevision)
fRequiredWidth= revisionWidth + authorWidth + 2;
else if (fShowAuthor)
fRequiredWidth= authorWidth + 1;
else
fRequiredWidth= revisionWidth + 1;
} else {
fRequiredWidth= 0;
}
}
return fRequiredWidth;
}
/**
* Enables showing the revision id.
*
* @param show <code>true</code> to show the revision, <code>false</code> to hide it
*/
public void showRevisionId(boolean show) {
if (fShowRevision != show) {
fRequiredWidth= -1;
fRevisionIdChars= 0;
fShowRevision= show;
postRedraw();
}
}
/**
* Enables showing the revision author.
*
* @param show <code>true</code> to show the author, <code>false</code> to hide it
*/
public void showRevisionAuthor(boolean show) {
if (fShowAuthor != show) {
fRequiredWidth= -1;
fRevisionIdChars= 0;
fShowAuthor= show;
postRedraw();
}
}
/**
* Adds a revision listener.
*
* @param listener the listener
* @since 3.3
*/
public void addRevisionListener(IRevisionListener listener) {
fRevisionListeners.add(listener);
}
/**
* Removes a revision listener.
*
* @param listener the listener
* @since 3.3
*/
public void removeRevisionListener(IRevisionListener listener) {
fRevisionListeners.remove(listener);
}
/**
* Informs the revision listeners about a change.
*
* @since 3.3
*/
private void informListeners() {
if (fRevisionInfo == null || fRevisionListeners.isEmpty())
return;
RevisionEvent event= new RevisionEvent(fRevisionInfo);
for (IRevisionListener listener : fRevisionListeners) {
listener.revisionInformationChanged(event);
}
}
}