/******************************************************************************* * Copyright (c) 2006, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.jface.internal.text.revisions; import java.util.ArrayList; import java.util.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.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.internal.text.html.BrowserInformationControl; import org.eclipse.jface.internal.text.html.HTMLPrinter; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.AbstractReusableInformationControlCreator; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DefaultInformationControl; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.jface.text.JFaceTextUtil; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.information.IInformationProviderExtension2; import org.eclipse.jface.text.revisions.IRevisionListener; import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension; import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension.RenderingMode; import org.eclipse.jface.text.revisions.Revision; import org.eclipse.jface.text.revisions.RevisionEvent; import org.eclipse.jface.text.revisions.RevisionInformation; import org.eclipse.jface.text.revisions.RevisionRange; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.CompositeRuler; import org.eclipse.jface.text.source.IAnnotationHover; import org.eclipse.jface.text.source.IAnnotationHoverExtension; import org.eclipse.jface.text.source.IAnnotationHoverExtension2; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IAnnotationModelExtension; import org.eclipse.jface.text.source.IAnnotationModelListener; import org.eclipse.jface.text.source.IChangeRulerColumn; import org.eclipse.jface.text.source.ILineDiffer; import org.eclipse.jface.text.source.ILineRange; import org.eclipse.jface.text.source.ISharedTextColors; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.IVerticalRulerColumn; import org.eclipse.jface.text.source.LineRange; /** * A strategy for painting the live annotate colors onto the vertical ruler column. It also manages * the revision hover. * * @since 3.2 */ public final class RevisionPainter { /** Tells whether this class is in debug mode. */ private static boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.jface.text.source/debug/RevisionRulerColumn")); //$NON-NLS-1$//$NON-NLS-2$ // RGBs provided by UI Designer private static final RGB BY_DATE_START_COLOR= new RGB(199, 134, 57); private static final RGB BY_DATE_END_COLOR= new RGB(241, 225, 206); /** * The annotations created to show a revision in the overview ruler. */ private static final class RevisionAnnotation extends Annotation { public RevisionAnnotation(String text) { super("org.eclipse.ui.workbench.texteditor.revisionAnnotation", false, text); //$NON-NLS-1$ } } /** * The color tool manages revision colors and computes shaded colors based on the relative age * and author of a revision. */ private final class ColorTool { /** * The average perceived intensity of a base color. 0 means black, 1 means white. A base * revision color perceived as light such as yellow will be darkened, while colors perceived * as dark such as blue will be lightened up. */ private static final float AVERAGE_INTENSITY= 0.5f; /** * The maximum shading in [0, 1] - this is the shade that the most recent revision will * receive. */ private static final float MAX_SHADING= 0.7f; /** * The minimum shading in [0, 1] - this is the shade that the oldest revision will receive. */ private static final float MIN_SHADING= 0.2f; /** * The shade for the focus boxes. */ private static final float FOCUS_COLOR_SHADING= 1f; /** * A list of {@link Long}, storing the age of each revision in a sorted list. */ private List fRevisions; /** * The stored shaded colors. */ private final Map fColors= new HashMap<>(); /** * The stored focus colors. */ private final Map fFocusColors= new HashMap<>(); /** * Sets the revision information, which is needed to compute the relative age of a revision. * * @param info the new revision info, null for none. */ public void setInfo(RevisionInformation info) { fRevisions= null; fColors.clear(); fFocusColors.clear(); if (info == null) return; List revisions= new ArrayList<>(); for (Revision revision : info.getRevisions()) { revisions.add(Long.valueOf(computeAge(revision))); } Collections.sort(revisions); fRevisions= revisions; } private RGB adaptColor(Revision revision, boolean focus) { RGB rgb; float scale; if (fRenderingMode == IRevisionRulerColumnExtension.AGE) { int index= computeAgeIndex(revision); if (index == -1 || fRevisions.size() == 0) { rgb= getBackground().getRGB(); } else { // gradient from intense red for most recent to faint yellow for oldest RGB[] gradient= Colors.palette(BY_DATE_START_COLOR, BY_DATE_END_COLOR, fRevisions.size()); rgb= gradient[gradient.length - index - 1]; } scale= 0.99f; } else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR) { rgb= revision.getColor(); rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY); scale= 0.6f; } else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR_SHADED_BY_AGE) { rgb= revision.getColor(); rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY); int index= computeAgeIndex(revision); int size= fRevisions.size(); // relative age: newest is 0, oldest is 1 // if there is only one revision, use an intermediate value to avoid extreme coloring if (index == -1 || size < 2) scale= 0.5f; else scale= (float) index / (size - 1); } else { Assert.isTrue(false); return null; // dummy } rgb= getShadedColor(rgb, scale, focus); return rgb; } private int computeAgeIndex(Revision revision) { long age= computeAge(revision); int index= fRevisions.indexOf(Long.valueOf(age)); return index; } private RGB getShadedColor(RGB color, float scale, boolean focus) { Assert.isLegal(scale >= 0.0); Assert.isLegal(scale <= 1.0); RGB background= getBackground().getRGB(); // normalize to lie within [MIN_SHADING, MAX_SHADING] // use more intense colors if the ruler is narrow (i.e. not showing line numbers) boolean makeIntense= getWidth() <= 15; float intensityShift= makeIntense ? 0.3f : 0f; float max= MAX_SHADING + intensityShift; float min= MIN_SHADING + intensityShift; scale= (max - min) * scale + min; // focus coloring if (focus) { scale += FOCUS_COLOR_SHADING; if (scale > 1) { background= new RGB(255 - background.red, 255 - background.green, 255 - background.blue); scale= 2 - scale; } } return Colors.blend(background, color, scale); } private long computeAge(Revision revision) { return revision.getDate().getTime(); } /** * Returns the color for a revision based on relative age and author. * * @param revision the revision * @param focus true to return the focus color * @return the color for a revision */ public RGB getColor(Revision revision, boolean focus) { Map map= focus ? fFocusColors : fColors; RGB color= 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 html is only an HTML fragment (has no * <html> 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("") != -1) //$NON-NLS-1$ // there is already a header return html; StringBuilder info= new StringBuilder(512 + html.length()); HTMLPrinter.insertPageProlog(info, 0, fgStyleSheet); info.append(html); HTMLPrinter.addPageEpilog(info); return info.toString(); } }; } return new DefaultInformationControl(parent, fIsFocusable); } @Override public boolean canReplace(IInformationControlCreator creator) { return creator.getClass() == getClass() && ((HoverInformationControlCreator) creator).fIsFocusable == fIsFocusable; } } private static final String fgStyleSheet= "/* Font definitions */\n" + //$NON-NLS-1$ "body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt {font-family: sans-serif; font-size: 9pt }\n" + //$NON-NLS-1$ "pre { font-family: monospace; font-size: 9pt }\n" + //$NON-NLS-1$ "\n" + //$NON-NLS-1$ "/* Margins */\n" + //$NON-NLS-1$ "body { overflow: auto; margin-top: 0; margin-bottom: 4; margin-left: 3; margin-right: 0 }\n" + //$NON-NLS-1$ "h1 { margin-top: 5; margin-bottom: 1 } \n" + //$NON-NLS-1$ "h2 { margin-top: 25; margin-bottom: 3 }\n" + //$NON-NLS-1$ "h3 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$ "h4 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$ "h5 { margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$ "p { margin-top: 10px; margin-bottom: 10px }\n" + //$NON-NLS-1$ "pre { margin-left: 6 }\n" + //$NON-NLS-1$ "ul { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ "li { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$ "li p { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$ "ol { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ "dl { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ "dt { margin-top: 0; margin-bottom: 0; font-weight: bold }\n" + //$NON-NLS-1$ "dd { margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$ "\n" + //$NON-NLS-1$ "/* Styles and colors */\n" + //$NON-NLS-1$ "a:link { color: hyperlinkColor }\n" + //$NON-NLS-1$ "a:hover { color: activeHyperlinkColor; }\n" + //$NON-NLS-1$ "a:visited { text-decoration: underline }\n" + //$NON-NLS-1$ "h4 { font-style: italic }\n" + //$NON-NLS-1$ "strong { font-weight: bold }\n" + //$NON-NLS-1$ "em { font-style: italic }\n" + //$NON-NLS-1$ "var { font-style: italic }\n" + //$NON-NLS-1$ "th { font-weight: bold }\n" + //$NON-NLS-1$ ""; //$NON-NLS-1$ /** * The revision hover displays information about the currently selected revision. */ private final class RevisionHover implements IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2, IInformationProviderExtension2 { @Override public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) { Object info= getHoverInfo(sourceViewer, getHoverLineRange(sourceViewer, lineNumber), 0); return info == null ? null : info.toString(); } @Override public IInformationControlCreator getHoverControlCreator() { RevisionInformation revisionInfo= fRevisionInfo; if (revisionInfo != null) { IInformationControlCreator creator= revisionInfo.getHoverControlCreator(); if (creator != null) return creator; } return new HoverInformationControlCreator(false); } @Override public boolean canHandleMouseCursor() { return false; } @Override public boolean canHandleMouseWheel() { return true; } @Override public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) { RevisionRange range= getRange(lineRange.getStartLine()); Object info= range == null ? null : range.getRevision().getHoverInfo(); return info; } @Override public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) { RevisionRange range= getRange(lineNumber); return range == null ? null : new LineRange(lineNumber, 1); } @Override public IInformationControlCreator getInformationPresenterControlCreator() { RevisionInformation revisionInfo= fRevisionInfo; if (revisionInfo != null) { IInformationControlCreator creator= revisionInfo.getInformationPresenterControlCreator(); if (creator != null) return creator; } return new HoverInformationControlCreator(true); } } /* Listeners and helpers. */ /** The shared color provider. */ private final ISharedTextColors fSharedColors; /** The color tool. */ private final ColorTool fColorTool= new ColorTool(); /** The mouse handler. */ private final MouseHandler fMouseHandler= new MouseHandler(); /** The hover. */ private final RevisionHover fHover= new RevisionHover(); /** The annotation listener. */ private final AnnotationListener fAnnotationListener= new AnnotationListener(); /** The selection provider. */ private final RevisionSelectionProvider fRevisionSelectionProvider= new RevisionSelectionProvider(this); /** * The list of revision listeners. * @since 3.3. */ private final ListenerList 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 null. */ 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 null. */ 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, null if none. */ private RevisionRange fFocusRange= null; /** The current focus revision, null if none. */ private Revision fFocusRevision= null; /** * The currently selected revision, null 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; /** true if the mouse wheel handler is installed, false 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; /** * true to show revision ids, false otherwise. * @since 3.3 */ private boolean fShowRevision= false; /** * true to show the author, false otherwise. * @since 3.3 */ private boolean fShowAuthor= false; /** * The author inset in pixels for when author *and* revision id are shown. * @since 3.3 */ private int fAuthorInset; /** * The remembered ruler width (as changing the ruler width triggers recomputation of the colors. * @since 3.3 */ private int fLastWidth= -1; /** * The zoom level for the current painting operation. Workaround for bug 516293. * @since 3.12 */ private int fZoom= 100; /** * Creates a new revision painter for a vertical ruler column. * * @param column the column that will delegate{@link #paint(GC, ILineRange) painting} to the * newly created painter. * @param sharedColors a shared colors object to store shaded colors in */ public RevisionPainter(IVerticalRulerColumn column, ISharedTextColors sharedColors) { Assert.isLegal(column != null); Assert.isLegal(sharedColors != null); fColumn= column; fSharedColors= sharedColors; } /** * Sets the revision information to be drawn and triggers a redraw. * * @param info the revision information to show, null 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, null for the platform's list * background */ public void setBackground(Color background) { fBackground= background; } /** * Sets the parent ruler - the delegating column must call this method as soon as it creates its * control. * * @param parentRuler the parent ruler */ public void setParentRuler(CompositeRuler parentRuler) { fParentRuler= parentRuler; } /** * Sets the zoom level for the current painting operation. Workaround for bug 516293. * * @param zoom the zoom to set * @since 3.12 */ public void setZoom(int zoom) { fZoom= zoom; } private int autoScaleUp(int value) { return value * fZoom / 100; } /** * Delegates the painting of the quick diff colors to this painter. The painter will draw the * color boxes onto the passed {@link GC} for all model (document) lines in * visibleModelLines. * * @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 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(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { handleDispose(); } }); fRevisionSelectionProvider.install(fViewer); } /** * Returns true if the column is fully connected. * * @return true if the column is fully connected, false otherwise */ private boolean isConnected() { return fControl != null; } /** * Sets the annotation model. * * @param model the annotation model, possibly null * @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 null 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 gc. * * @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 gc. 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 GC 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 * fCachedTextWidget * @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 true 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 * null if there is none. * * @param line the line of interest * @return the corresponding RevisionRange or null */ private RevisionRange getRange(int line) { List 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 RevisionRanges 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= 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 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 true if range contains line. 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 line * @param line the line the line * @return true if range contains line, * false 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 range */ 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 range 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 range */ private Rectangle computeBoxBounds(ILineRange range) { int y1= fWidget.getLinePixel(range.getStartLine()); int y2= fWidget.getLinePixel(range.getStartLine() + range.getNumberOfLines()); return new Rectangle(0, autoScaleUp(y1), autoScaleUp(getWidth()), autoScaleUp(y2 - y1 - 1)); } /** * Shows (or hides) the overview annotations. */ private void updateOverviewAnnotations() { if (fAnnotationModel == null) return; Revision revision= fFocusRevision != null ? fFocusRevision : fSelectedRevision; Map 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 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 range * @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, null 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, null 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 (null for no focus) * @param nextRange the new focus range (null 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 (null for no focus) * @param nextRevision the new focus revision (null 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 true 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 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 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(new Runnable() { @Override public void run() { redraw(); } }); } } } /** * Translates a y coordinate in the pixel coordinates of the column's control to a document line * number. * * @param y the y coordinate * @return the corresponding document line, -1 for no line * @see CompositeRuler#toDocumentLineNumber(int) */ private int toDocumentLineNumber(int y) { return fParentRuler.toDocumentLineNumber(y); } /** * Triggers redrawing of the column. */ private void redraw() { fColumn.redraw(); } /** * Triggers immediate redrawing of the entire column - use with care. */ private void immediateUpdate() { fParentRuler.immediateUpdate(); } /** * Returns the width of the column. * * @return the width of the column */ private int getWidth() { return fColumn.getWidth(); } /** * Returns the System background color for list widgets. * * @return the System background color for list widgets */ private Color getBackground() { if (fBackground == null) return fWidget.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND); return fBackground; } /** * Sets the hover later returned by {@link #getHover()}. * * @param hover the hover */ public void setHover(IAnnotationHover hover) { // TODO ignore for now - must make revision hover settable from outside } /** * Returns true if the receiver can provide a hover for a certain document line. * * @param activeLine the document line of interest * @return true 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 null for none. * * @param offset the document offset * @return the revision at offset, or null 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 true if a revision model has been set, false otherwise. * * @return true if a revision model has been set, false 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 true to show the revision, false 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 true to show the author, false 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); } } }