view dwtx/jface/internal/text/revisions/RevisionPainter.d @ 153:f70d9508c95c

Fix java Collection imports
author Frank Benoit <benoit@tionex.de>
date Mon, 25 Aug 2008 00:27:31 +0200
parents 5cf141e43417
children 25f1f92fa3df
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2006, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 * Port to the D programming language:
 *     Frank Benoit <benoit@tionex.de>
 *******************************************************************************/
module dwtx.jface.internal.text.revisions.RevisionPainter;

import dwtx.jface.internal.text.revisions.HunkComputer; // packageimport
import dwtx.jface.internal.text.revisions.LineIndexOutOfBoundsException; // packageimport
import dwtx.jface.internal.text.revisions.Hunk; // packageimport
import dwtx.jface.internal.text.revisions.Colors; // packageimport
import dwtx.jface.internal.text.revisions.ChangeRegion; // packageimport
import dwtx.jface.internal.text.revisions.Range; // packageimport
import dwtx.jface.internal.text.revisions.RevisionSelectionProvider; // packageimport


import dwt.dwthelper.utils;

import dwtx.dwtxhelper.Collection;









import dwt.DWT;
import dwt.custom.StyledText;
import dwt.events.DisposeEvent;
import dwt.events.DisposeListener;
import dwt.events.MouseEvent;
import dwt.events.MouseMoveListener;
import dwt.events.MouseTrackListener;
import dwt.graphics.Color;
import dwt.graphics.FontMetrics;
import dwt.graphics.GC;
import dwt.graphics.Point;
import dwt.graphics.RGB;
import dwt.graphics.Rectangle;
import dwt.widgets.Canvas;
import dwt.widgets.Control;
import dwt.widgets.Display;
import dwt.widgets.Event;
import dwt.widgets.Listener;
import dwt.widgets.Shell;
import dwtx.core.runtime.Assert;
import dwtx.core.runtime.ListenerList;
import dwtx.core.runtime.Platform;
import dwtx.jface.internal.text.html.BrowserInformationControl;
import dwtx.jface.internal.text.html.HTMLPrinter;
import dwtx.jface.resource.JFaceResources;
import dwtx.jface.text.AbstractReusableInformationControlCreator;
import dwtx.jface.text.BadLocationException;
import dwtx.jface.text.DefaultInformationControl;
import dwtx.jface.text.IDocument;
import dwtx.jface.text.IInformationControl;
import dwtx.jface.text.IInformationControlCreator;
import dwtx.jface.text.IRegion;
import dwtx.jface.text.ITextViewer;
import dwtx.jface.text.ITextViewerExtension5;
import dwtx.jface.text.JFaceTextUtil;
import dwtx.jface.text.Position;
import dwtx.jface.text.Region;
import dwtx.jface.text.information.IInformationProviderExtension2;
import dwtx.jface.text.revisions.IRevisionListener;
import dwtx.jface.text.revisions.IRevisionRulerColumnExtension;
import dwtx.jface.text.revisions.Revision;
import dwtx.jface.text.revisions.RevisionEvent;
import dwtx.jface.text.revisions.RevisionInformation;
import dwtx.jface.text.revisions.RevisionRange;
import dwtx.jface.text.revisions.IRevisionRulerColumnExtension.RenderingMode;
import dwtx.jface.text.source.Annotation;
import dwtx.jface.text.source.CompositeRuler;
import dwtx.jface.text.source.IAnnotationHover;
import dwtx.jface.text.source.IAnnotationHoverExtension;
import dwtx.jface.text.source.IAnnotationHoverExtension2;
import dwtx.jface.text.source.IAnnotationModel;
import dwtx.jface.text.source.IAnnotationModelExtension;
import dwtx.jface.text.source.IAnnotationModelListener;
import dwtx.jface.text.source.IChangeRulerColumn;
import dwtx.jface.text.source.ILineDiffer;
import dwtx.jface.text.source.ILineRange;
import dwtx.jface.text.source.ISharedTextColors;
import dwtx.jface.text.source.ISourceViewer;
import dwtx.jface.text.source.IVerticalRulerColumn;
import dwtx.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 bool DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("dwtx.jface.text.source/debug/RevisionRulerColumn")); //$NON-NLS-1$//$NON-NLS-2$

    // RGBs provided by UI Designer
    private static const RGB BY_DATE_START_COLOR= new RGB(199, 134, 57);
    private static const 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 : Annotation {
        public this(String text) {
            super("dwtx.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 const float AVERAGE_INTENSITY= 0.5f;
        /**
         * The maximum shading in [0, 1] - this is the shade that the most recent revision will
         * receive.
         */
        private static const float MAX_SHADING= 0.7f;
        /**
         * The minimum shading in [0, 1] - this is the shade that the oldest revision will receive.
         */
        private static const float MIN_SHADING= 0.2f;
        /**
         * The shade for the focus boxes.
         */
        private static const float FOCUS_COLOR_SHADING= 1f;


        /**
         * A list of {@link Long}, storing the age of each revision in a sorted list.
         */
        private List fRevisions;
        /**
         * The stored shaded colors.
         */
        private const Map fColors= new HashMap();
        /**
         * The stored focus colors.
         */
        private const Map fFocusColors= new HashMap();

        /**
         * Sets the revision information, which is needed to compute the relative age of a revision.
         *
         * @param info the new revision info, <code>null</code> for none.
         */
        public void setInfo(RevisionInformation info) {
            fRevisions= null;
            fColors.clear();
            fFocusColors.clear();

            if (info is null)
                return;
            List revisions= new ArrayList();
            for (Iterator it= info.getRevisions().iterator(); it.hasNext();) {
                Revision revision= cast(Revision) it.next();
                revisions.add(new Long(computeAge(revision)));
            }
            Collections.sort(revisions);
            fRevisions= revisions;
        }

        private RGB adaptColor(Revision revision, bool focus) {
            RGB rgb;
            float scale;
            if (fRenderingMode is IRevisionRulerColumnExtension.AGE) {
                int index= computeAgeIndex(revision);
                if (index is -1 || fRevisions.size() is 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 is IRevisionRulerColumnExtension.AUTHOR) {
                rgb= revision.getColor();
                rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY);
                scale= 0.6f;
            } else if (fRenderingMode is 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 is -1 || size < 2)
                    scale= 0.5f;
                else
                    scale= cast(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(new Long(age));
            return index;
        }

        private RGB getShadedColor(RGB color, float scale, bool 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)
            bool 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, bool focus) {
            Map map= focus ? fFocusColors : fColors;
            RGB color= cast(RGB) map.get(revision);
            if (color !is 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 : MouseMoveListener, MouseTrackListener, Listener {

        private RevisionRange fMouseDownRegion;

        private void handleMouseUp(Event e) {
            if (e.button is 1) {
                RevisionRange upRegion= fFocusRange;
                RevisionRange downRegion= fMouseDownRegion;
                fMouseDownRegion= null;

                if (upRegion is downRegion) {
                    Revision revision= upRegion is null ? null : upRegion.getRevision();
                    if (revision is fSelectedRevision)
                        revision= null; // deselect already selected revision
                    handleRevisionSelected(revision);
                }
            }
        }

        private void handleMouseDown(Event e) {
            if (e.button is 3)
                updateFocusRevision(null); // kill any focus as the ctx menu is going to show
            if (e.button is 1) {
                fMouseDownRegion= fFocusRange;
                postRedraw();
            }
        }

        /*
         * @see dwt.widgets.Listener#handleEvent(dwt.widgets.Event)
         */
        public void handleEvent(Event event) {
            switch (event.type) {
                case DWT.MouseWheel:
                    handleMouseWheel(event);
                    break;
                case DWT.MouseDown:
                    handleMouseDown(event);
                    break;
                case DWT.MouseUp:
                    handleMouseUp(event);
                    break;
                default:
                    Assert.isLegal(false);
            }
        }

        /*
         * @see dwt.events.MouseTrackListener#mouseEnter(dwt.events.MouseEvent)
         */
        public void mouseEnter(MouseEvent e) {
            updateFocusLine(toDocumentLineNumber(e.y));
        }

        /*
         * @see dwt.events.MouseTrackListener#mouseExit(dwt.events.MouseEvent)
         */
        public void mouseExit(MouseEvent e) {
            updateFocusLine(-1);
        }

        /*
         * @see dwt.events.MouseTrackListener#mouseHover(dwt.events.MouseEvent)
         */
        public void mouseHover(MouseEvent e) {
        }

        /*
         * @see dwt.events.MouseMoveListener#mouseMove(dwt.events.MouseEvent)
         */
        public void mouseMove(MouseEvent e) {
            updateFocusLine(toDocumentLineNumber(e.y));
        }
    }

    /**
     * Internal listener class that will update the ruler when the underlying model changes.
     */
    private class AnnotationListener : IAnnotationModelListener {
        /*
         * @see dwtx.jface.text.source.IAnnotationModelListener#modelChanged(dwtx.jface.text.source.IAnnotationModel)
         */
        public void modelChanged(IAnnotationModel model) {
            clearRangeCache();
            postRedraw();
        }

    }

    /**
     * The information control creator.
     */
    private static final class HoverInformationControlCreator : AbstractReusableInformationControlCreator {
        private bool fIsFocusable;

        public this(bool isFocusable) {
            fIsFocusable= isFocusable;
        }

        /*
         * @see dwtx.jface.internal.text.revisions.AbstractReusableInformationControlCreator#doCreateInformationControl(dwt.widgets.Shell)
         */
        protected IInformationControl doCreateInformationControl(Shell parent) {
            if (BrowserInformationControl.isAvailable(parent)) {
                return new class(parent, JFaceResources.DIALOG_FONT, fIsFocusable)  BrowserInformationControl {
                    /*
                     * @see dwtx.jface.internal.text.html.BrowserInformationControl#setInformation(java.lang.String)
                     * @since 3.3
                     */
                    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>") !is -1) //$NON-NLS-1$
                            // there is already a header
                            return html;

                        StringBuffer info= new StringBuffer(512 + html.length());
                        HTMLPrinter.insertPageProlog(info, 0, fgStyleSheet);
                        info.append(html);
                        HTMLPrinter.addPageEpilog(info);
                        return info.toString();
                    }

                };
            }
            return new DefaultInformationControl(parent, fIsFocusable);
        }

        /*
         * @see dwtx.jface.text.AbstractReusableInformationControlCreator#canReplace(dwtx.jface.text.IInformationControlCreator)
         */
        public bool canReplace(IInformationControlCreator creator) {
            return creator.getClass() is getClass()
                    && (cast(HoverInformationControlCreator) creator).fIsFocusable is 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: #0000FF }\n" + //$NON-NLS-1$
        "a:hover         { color: #000080 }\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 : IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2, IInformationProviderExtension2 {

        /*
         * @see dwtx.jface.text.source.IAnnotationHover#getHoverInfo(dwtx.jface.text.source.ISourceViewer,
         *      int)
         */
        public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) {
            Object info= getHoverInfo(sourceViewer, getHoverLineRange(sourceViewer, lineNumber), 0);
            return info is null ? null : info.toString();
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationHoverExtension#getHoverControlCreator()
         */
        public IInformationControlCreator getHoverControlCreator() {
            RevisionInformation revisionInfo= fRevisionInfo;
            if (revisionInfo !is null) {
                IInformationControlCreator creator= revisionInfo.getHoverControlCreator();
                if (creator !is null)
                    return creator;
            }
            return new HoverInformationControlCreator(false);
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationHoverExtension#canHandleMouseCursor()
         */
        public bool canHandleMouseCursor() {
            return false;
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationHoverExtension2#canHandleMouseWheel()
         */
        public bool canHandleMouseWheel() {
            return true;
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationHoverExtension#getHoverInfo(dwtx.jface.text.source.ISourceViewer,
         *      dwtx.jface.text.source.ILineRange, int)
         */
        public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) {
            RevisionRange range= getRange(lineRange.getStartLine());
            Object info= range is null ? null : range.getRevision().getHoverInfo();
            return info;
        }

        /*
         * @see dwtx.jface.text.source.IAnnotationHoverExtension#getHoverLineRange(dwtx.jface.text.source.ISourceViewer,
         *      int)
         */
        public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) {
            RevisionRange range= getRange(lineNumber);
            return range is null ? null : new LineRange(lineNumber, 1);
        }

        /*
         * @see dwtx.jface.text.information.IInformationProviderExtension2#getInformationPresenterControlCreator()
         */
        public IInformationControlCreator getInformationPresenterControlCreator() {
            RevisionInformation revisionInfo= fRevisionInfo;
            if (revisionInfo !is null) {
                IInformationControlCreator creator= revisionInfo.getInformationPresenterControlCreator();
                if (creator !is null)
                    return creator;
            }
            return new HoverInformationControlCreator(true);
        }
    }

    /* Listeners and helpers. */

    /** The shared color provider. */
    private const ISharedTextColors fSharedColors;
    /** The color tool. */
    private const ColorTool fColorTool= new ColorTool();
    /** The mouse handler. */
    private const MouseHandler fMouseHandler= new MouseHandler();
    /** The hover. */
    private const RevisionHover fHover= new RevisionHover();
    /** The annotation listener. */
    private const AnnotationListener fAnnotationListener= new AnnotationListener();
    /** The selection provider. */
    private const RevisionSelectionProvider fRevisionSelectionProvider= new RevisionSelectionProvider(this);
    /**
     * The list of revision listeners.
     * @since 3.3.
     */
    private const ListenerList 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 const 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 fRevisionRanges= null;
    /** The annotations created for the overview ruler temporary display. */
    private List fAnnotations= new ArrayList();

    /* State */

    /** The current focus line, -1 for none. */
    private int fFocusLine= -1;
    /** The current focus region, <code>null</code> if none. */
    private 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 bool 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 bool fShowRevision= false;
    /**
     * <code>true</code> to show the author, <code>false</code> otherwise.
     * @since 3.3
     */
    private bool 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;

    /**
     * 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 this(IVerticalRulerColumn column, ISharedTextColors sharedColors) {
        Assert.isLegal(column !is null);
        Assert.isLegal(sharedColors !is 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 !is info) {
            fRequiredWidth= -1;
            fRevisionIdChars= 0;
            fRevisionInfo= info;
            clearRangeCache();
            updateFocusRange(null);
            handleRevisionSelected(cast(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 !is null);
        if (fRenderingMode !is 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;
    }

    /**
     * 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 !is fLastWidth) {
            fColorTool.setInfo(fRevisionInfo);
            fLastWidth= width;
        }

        // draw change regions
        List/* <RevisionRange> */ranges= getRanges(visibleLines);
        for (Iterator it= ranges.iterator(); it.hasNext();) {
            RevisionRange region= cast(RevisionRange) it.next();
            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 is null)
            return;

        fViewer= fParentRuler.getTextViewer();
        if (fViewer is null)
            return;

        fWidget= fViewer.getTextWidget();
        if (fWidget is null)
            return;

        fControl= fColumn.getControl();
        if (fControl is null)
            return;

        fControl.addMouseTrackListener(fMouseHandler);
        fControl.addMouseMoveListener(fMouseHandler);
        fControl.addListener(DWT.MouseUp, fMouseHandler);
        fControl.addListener(DWT.MouseDown, fMouseHandler);
        fControl.addDisposeListener(new class()  DisposeListener {
            /*
             * @see dwt.events.DisposeListener#widgetDisposed(dwt.events.DisposeEvent)
             */
            public void widgetDisposed(DisposeEvent 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 bool isConnected() {
        return fControl !is 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 ( cast(IAnnotationModelExtension)model )
            diffModel= (cast(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 !is model)
            fAnnotationModel= model;
    }

    /**
     * Sets the line differ.
     *
     * @param differ the line differ or <code>null</code> if none
     */
    private void setDiffer(IAnnotationModel differ) {
        if ( cast(ILineDiffer)differ  || differ is null) {
            if (fLineDiffer !is differ) {
                if (fLineDiffer !is null)
                    (cast(IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
                fLineDiffer= cast(ILineDiffer) differ;
                if (fLineDiffer !is null)
                    (cast(IAnnotationModel) fLineDiffer).addAnnotationModelListener(fAnnotationListener);
            }
        }
    }

    /**
     * Disposes of the painter's resources.
     */
    private void handleDispose() {
        updateFocusLine(-1);

        if (fLineDiffer !is null) {
            (cast(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 is null)
            return;

        Revision revision= range.getRevision();
        bool drawArmedFocus= range is fMouseHandler.fMouseDownRegion;
        bool drawSelection= !drawArmedFocus && revision is fSelectedRevision;
        bool drawFocus= !drawSelection && !drawArmedFocus && revision is 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) {
        /*
         * 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, bool 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 ranges= getRangeCache();

        if (ranges.isEmpty() || line is -1)
            return null;

        for (Iterator it= ranges.iterator(); it.hasNext();) {
            RevisionRange range= cast(RevisionRange) it.next();
            if (contains(range, line))
                return range;
        }

        // line may be right after the last region
        RevisionRange lastRegion= cast(RevisionRange) ranges.get(ranges.size() - 1);
        if (line is 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 getRanges(ILineRange lines) {
        List 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= cast(RevisionRange) ranges.get(i);
            int rangeEnd= end(range);
            if (first is -1 && rangeEnd > lines.getStartLine())
                first= i;
            if (first !is -1 && rangeEnd > end) {
                last= i;
                break;
            }
        }
        if (first is -1)
            return Collections.EMPTY_LIST;
        if (last is -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 List getRangeCache() {
        if (fRevisionRanges is null) {
            if (fRevisionInfo is null) {
                fRevisionRanges= Collections.EMPTY_LIST;
            } 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 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 bool 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 ( cast(ITextViewerExtension5)fViewer ) {
            ITextViewerExtension5 extension= cast(ITextViewerExtension5) fViewer;
            int modelEndLine= end(range);
            for (int modelLine= range.getStartLine(); modelLine < modelEndLine; modelLine++) {
                int widgetLine= extension.modelLine2WidgetLine(modelLine);
                if (widgetLine !is -1) {
                    if (widgetStartLine is -1)
                        widgetStartLine= widgetLine;
                    widgetEndLine= widgetLine;
                }
            }
        } else {
            IRegion region= fViewer.getVisibleRegion();
            IDocument document= fViewer.getDocument();
            try {
                int visibleStartLine= document.getLineOfOffset(region.getOffset());
                int visibleEndLine= document.getLineOfOffset(region.getOffset() + region.getLength());
                widgetStartLine= Math.max(0, range.getStartLine() - visibleStartLine);
                widgetEndLine= Math.min(visibleEndLine, end(range) - 1);
            } catch (BadLocationException x) {
                x.printStackTrace();
                // ignore and return null
            }
        }
        if (widgetStartLine is -1 || widgetEndLine is -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 is null)
            return;

        Revision revision= fFocusRevision !is null ? fFocusRevision : fSelectedRevision;

        Map added= null;
        if (revision !is null) {
            added= new HashMap();
            for (Iterator it= revision.getRegions().iterator(); it.hasNext();) {
                RevisionRange range= cast(RevisionRange) it.next();
                try {
                    IRegion charRegion= toCharRegion(range);
                    Position position= new Position(charRegion.getOffset(), charRegion.getLength());
                    Annotation annotation= new RevisionAnnotation(revision.getId());
                    added.put(annotation, position);
                } catch (BadLocationException x) {
                    // ignore - document was changed, show no annotations
                }
            }
        }

        if ( cast(IAnnotationModelExtension)fAnnotationModel ) {
            IAnnotationModelExtension ext= cast(IAnnotationModelExtension) fAnnotationModel;
            ext.replaceAnnotations((Annotation[]) fAnnotations.toArray(new Annotation[fAnnotations.size()]), added);
        } else {
            for (Iterator it= fAnnotations.iterator(); it.hasNext();) {
                Annotation annotation= cast(Annotation) it.next();
                fAnnotationModel.removeAnnotation(annotation);
            }
            if (added !is null) {
                for (Iterator it= added.entrySet().iterator(); it.hasNext();) {
                    Entry entry= cast(Entry) it.next();
                    fAnnotationModel.addAnnotation(cast(Annotation) entry.getKey(), cast(Position) entry.getValue());
                }
            }
        }
        fAnnotations.clear();
        if (added !is 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)  {
        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);
        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 !is null);
        if (fRevisionInfo is null)
            return;

        for (Iterator it= fRevisionInfo.getRevisions().iterator(); it.hasNext();) {
            Revision revision= cast(Revision) it.next();
            if (id.equals(revision.getId())) {
                handleRevisionSelected(revision);
                return;
            }
        }

        // clear selection if it does not exist
        handleRevisionSelected(cast(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 !is 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 !is 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 is null ? null : nextRange.getRevision();
        updateFocusRevision(revision);
    }

    private void updateFocusRevision(Revision revision) {
        if (fFocusRevision !is 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(DWT.MouseWheel, fMouseHandler);
        fWheelHandlerInstalled= false;
    }

    /**
     * Installs the mouse wheel handler.
     */
    private void installWheelHandler() {
        if (fFocusRevision !is 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(DWT.MouseWheel, fMouseHandler);
            fWheelHandlerInstalled= true;
        }
    }

    /**
     * Handles a mouse wheel event.
     *
     * @param event the mouse wheel event
     */
    private void handleMouseWheel(Event event) {
        bool up= event.count > 0;
        int documentHoverLine= fFocusLine;

        ILineRange nextWidgetRange= null;
        ILineRange last= null;
        List ranges= fFocusRevision.getRegions();
        if (up) {
            for (Iterator it= ranges.iterator(); it.hasNext();) {
                RevisionRange range= cast(RevisionRange) it.next();
                ILineRange widgetRange= modelLinesToWidgetLines(range);
                if (contains(range, documentHoverLine)) {
                    nextWidgetRange= last;
                    break;
                }
                if (widgetRange !is null)
                    last= widgetRange;
            }
        } else {
            for (ListIterator it= ranges.listIterator(ranges.size()); it.hasPrevious();) {
                RevisionRange range= cast(RevisionRange) it.previous();
                ILineRange widgetRange= modelLinesToWidgetLines(range);
                if (contains(range, documentHoverLine)) {
                    nextWidgetRange= last;
                    break;
                }
                if (widgetRange !is null)
                    last= widgetRange;
            }
        }

        if (nextWidgetRange is 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 !is null) {
                d.asyncExec(new class()  Runnable {
                    public void run() {
                        redraw();
                    }
                });
            }
        }
    }

    /**
     * Translates a y coordinate in the pixel coordinates of the column's control to a document line
     * number.
     *
     * @param y the y coordinate
     * @return the corresponding document line, -1 for no line
     * @see CompositeRuler#toDocumentLineNumber(int)
     */
    private int toDocumentLineNumber(int y) {
        return fParentRuler.toDocumentLineNumber(y);
    }

    /**
     * Triggers redrawing of the column.
     */
    private void redraw() {
        fColumn.redraw();
    }

    /**
     * Triggers immediate redrawing of the entire column - use with care.
     */
    private void immediateUpdate() {
        fParentRuler.immediateUpdate();
    }

    /**
     * Returns the width of the column.
     *
     * @return the width of the column
     */
    private int getWidth() {
        return fColumn.getWidth();
    }

    /**
     * Returns the System background color for list widgets.
     *
     * @return the System background color for list widgets
     */
    private Color getBackground() {
        if (fBackground is null)
            return fWidget.getDisplay().getSystemColor(DWT.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 bool hasHover(int activeLine) {
        return cast(ISourceViewer)fViewer && fHover.getHoverLineRange(cast(ISourceViewer) fViewer, activeLine) !is 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 !is -1) {
            RevisionRange range= getRange(line);
            if (range !is 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 bool hasInformation() {
        return fRevisionInfo !is 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 is -1) {
            if (hasInformation() && (fShowRevision || fShowAuthor)) {
                int revisionWidth= 0;
                int authorWidth= 0;
                for (Iterator it= fRevisionInfo.getRevisions().iterator(); it.hasNext();) {
                    Revision revision= cast(Revision) it.next();
                    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(bool show) {
        if (fShowRevision !is 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(bool show) {
        if (fShowAuthor !is 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 is null || fRevisionListeners.isEmpty())
            return;

        RevisionEvent event= new RevisionEvent(fRevisionInfo);
        Object[] listeners= fRevisionListeners.getListeners();
        for (int i= 0; i < listeners.length; i++) {
            IRevisionListener listener= cast(IRevisionListener) listeners[i];
            listener.revisionInformationChanged(event);
        }
    }
}