/*******************************************************************************
 * 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.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.IPropertyChangeListener;

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<>();

		/** Cached gradient start and stop values. */
		private RGB fGradientStart;

		private RGB fGradientStop;

		/**
		 * 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();
			fGradientStart= null;
			fGradientStop= null;
			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 getRGB(String key, RGB defaultValue) {
			RGB rgb= JFaceResources.getColorRegistry().getRGB(key);
			return rgb != null ? rgb : defaultValue;
		}

		private RGB adaptColor(Revision revision, boolean focus) {
			RGB rgb;
			float scale;
			if (fRenderingMode == IRevisionRulerColumnExtension.AGE) {
				int index= computeAgeIndex(revision);
				if (index == -1 || fRevisions.isEmpty()) {
					rgb= getBackground().getRGB();
				} else {
					if (fGradientStart == null) {
						fGradientStart= getRGB(JFacePreferences.REVISION_NEWEST_COLOR, BY_DATE_START_COLOR);
					}
					if (fGradientStop == null) {
						fGradientStop= getRGB(JFacePreferences.REVISION_OLDEST_COLOR, BY_DATE_END_COLOR);
					}
					// gradient from most recent to oldest
					RGB[] gradient= Colors.palette(fGradientStart, fGradientStop, 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).contains("<html>")) //$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 JFace preference store, or {@code null} if there is none. */
	private IPreferenceStore fPreferences;

	/** Listens on {@link #fPreferences} to update the ruler when colors change. */
	private final IPropertyChangeListener fColorListener= event -> {
		String property= event.getProperty();
		if (property == null) {
			return;
		}
		switch (property) {
			case JFacePreferences.REVISION_NEWEST_COLOR:
			case JFacePreferences.REVISION_OLDEST_COLOR:
				fLastWidth= -1; // Recalculate colors
				postRedraw();
				break;
			default:
				break;
		}
	};

	/**
	 * 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;
		if (parentRuler != null) {
			fPreferences= JFacePreferences.getPreferenceStore();
			if (fPreferences != null) {
				fPreferences.addPropertyChangeListener(fColorListener);
			}
		} else if (fPreferences != null) {
			fPreferences.removePropertyChangeListener(fColorListener);
			fPreferences= null;
		}
	}

	/**
	 * 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, y1, getWidth(), 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(this::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);
		}
	}
}