| /******************************************************************************* |
| * Copyright (c) 2006, 2018 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jface.internal.text.revisions; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.MouseTrackListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.FontMetrics; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.RGB; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Canvas; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Shell; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.ListenerList; |
| import org.eclipse.core.runtime.Platform; |
| |
| import org.eclipse.jface.internal.text.html.BrowserInformationControl; |
| import org.eclipse.jface.internal.text.html.HTMLPrinter; |
| import org.eclipse.jface.resource.JFaceResources; |
| |
| import org.eclipse.jface.text.AbstractReusableInformationControlCreator; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DefaultInformationControl; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IInformationControl; |
| import org.eclipse.jface.text.IInformationControlCreator; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.ITextViewerExtension5; |
| import org.eclipse.jface.text.JFaceTextUtil; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.information.IInformationProviderExtension2; |
| import org.eclipse.jface.text.revisions.IRevisionListener; |
| import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension; |
| import org.eclipse.jface.text.revisions.IRevisionRulerColumnExtension.RenderingMode; |
| import org.eclipse.jface.text.revisions.Revision; |
| import org.eclipse.jface.text.revisions.RevisionEvent; |
| import org.eclipse.jface.text.revisions.RevisionInformation; |
| import org.eclipse.jface.text.revisions.RevisionRange; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.CompositeRuler; |
| import org.eclipse.jface.text.source.IAnnotationHover; |
| import org.eclipse.jface.text.source.IAnnotationHoverExtension; |
| import org.eclipse.jface.text.source.IAnnotationHoverExtension2; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension; |
| import org.eclipse.jface.text.source.IAnnotationModelListener; |
| import org.eclipse.jface.text.source.IChangeRulerColumn; |
| import org.eclipse.jface.text.source.ILineDiffer; |
| import org.eclipse.jface.text.source.ILineRange; |
| import org.eclipse.jface.text.source.ISharedTextColors; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.jface.text.source.IVerticalRulerColumn; |
| import org.eclipse.jface.text.source.LineRange; |
| |
| |
| /** |
| * A strategy for painting the live annotate colors onto the vertical ruler column. It also manages |
| * the revision hover. |
| * |
| * @since 3.2 |
| */ |
| public final class RevisionPainter { |
| /** Tells whether this class is in debug mode. */ |
| private static boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.jface.text.source/debug/RevisionRulerColumn")); //$NON-NLS-1$//$NON-NLS-2$ |
| |
| // RGBs provided by UI Designer |
| private static final RGB BY_DATE_START_COLOR= new RGB(199, 134, 57); |
| private static final RGB BY_DATE_END_COLOR= new RGB(241, 225, 206); |
| |
| |
| /** |
| * The annotations created to show a revision in the overview ruler. |
| */ |
| private static final class RevisionAnnotation extends Annotation { |
| public RevisionAnnotation(String text) { |
| super("org.eclipse.ui.workbench.texteditor.revisionAnnotation", false, text); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * The color tool manages revision colors and computes shaded colors based on the relative age |
| * and author of a revision. |
| */ |
| private final class ColorTool { |
| /** |
| * The average perceived intensity of a base color. 0 means black, 1 means white. A base |
| * revision color perceived as light such as yellow will be darkened, while colors perceived |
| * as dark such as blue will be lightened up. |
| */ |
| private static final float AVERAGE_INTENSITY= 0.5f; |
| /** |
| * The maximum shading in [0, 1] - this is the shade that the most recent revision will |
| * receive. |
| */ |
| private static final float MAX_SHADING= 0.7f; |
| /** |
| * The minimum shading in [0, 1] - this is the shade that the oldest revision will receive. |
| */ |
| private static final float MIN_SHADING= 0.2f; |
| /** |
| * The shade for the focus boxes. |
| */ |
| private static final float FOCUS_COLOR_SHADING= 1f; |
| |
| |
| /** |
| * A list of {@link Long}, storing the age of each revision in a sorted list. |
| */ |
| private List<Long> fRevisions; |
| /** |
| * The stored shaded colors. |
| */ |
| private final Map<Revision, RGB> fColors= new HashMap<>(); |
| /** |
| * The stored focus colors. |
| */ |
| private final Map<Revision, RGB> fFocusColors= new HashMap<>(); |
| |
| /** |
| * Sets the revision information, which is needed to compute the relative age of a revision. |
| * |
| * @param info the new revision info, <code>null</code> for none. |
| */ |
| public void setInfo(RevisionInformation info) { |
| fRevisions= null; |
| fColors.clear(); |
| fFocusColors.clear(); |
| |
| if (info == null) |
| return; |
| List<Long> revisions= new ArrayList<>(); |
| for (Revision revision : info.getRevisions()) { |
| revisions.add(Long.valueOf(computeAge(revision))); |
| } |
| Collections.sort(revisions); |
| fRevisions= revisions; |
| } |
| |
| private RGB adaptColor(Revision revision, boolean focus) { |
| RGB rgb; |
| float scale; |
| if (fRenderingMode == IRevisionRulerColumnExtension.AGE) { |
| int index= computeAgeIndex(revision); |
| if (index == -1 || fRevisions.size() == 0) { |
| rgb= getBackground().getRGB(); |
| } else { |
| // gradient from intense red for most recent to faint yellow for oldest |
| RGB[] gradient= Colors.palette(BY_DATE_START_COLOR, BY_DATE_END_COLOR, fRevisions.size()); |
| rgb= gradient[gradient.length - index - 1]; |
| } |
| scale= 0.99f; |
| } else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR) { |
| rgb= revision.getColor(); |
| rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY); |
| scale= 0.6f; |
| } else if (fRenderingMode == IRevisionRulerColumnExtension.AUTHOR_SHADED_BY_AGE) { |
| rgb= revision.getColor(); |
| rgb= Colors.adjustBrightness(rgb, AVERAGE_INTENSITY); |
| int index= computeAgeIndex(revision); |
| int size= fRevisions.size(); |
| // relative age: newest is 0, oldest is 1 |
| // if there is only one revision, use an intermediate value to avoid extreme coloring |
| if (index == -1 || size < 2) |
| scale= 0.5f; |
| else |
| scale= (float) index / (size - 1); |
| } else { |
| Assert.isTrue(false); |
| return null; // dummy |
| } |
| rgb= getShadedColor(rgb, scale, focus); |
| return rgb; |
| } |
| |
| private int computeAgeIndex(Revision revision) { |
| long age= computeAge(revision); |
| int index= fRevisions.indexOf(Long.valueOf(age)); |
| return index; |
| } |
| |
| private RGB getShadedColor(RGB color, float scale, boolean focus) { |
| Assert.isLegal(scale >= 0.0); |
| Assert.isLegal(scale <= 1.0); |
| RGB background= getBackground().getRGB(); |
| |
| // normalize to lie within [MIN_SHADING, MAX_SHADING] |
| // use more intense colors if the ruler is narrow (i.e. not showing line numbers) |
| boolean makeIntense= getWidth() <= 15; |
| float intensityShift= makeIntense ? 0.3f : 0f; |
| float max= MAX_SHADING + intensityShift; |
| float min= MIN_SHADING + intensityShift; |
| scale= (max - min) * scale + min; |
| |
| // focus coloring |
| if (focus) { |
| scale += FOCUS_COLOR_SHADING; |
| if (scale > 1) { |
| background= new RGB(255 - background.red, 255 - background.green, 255 - background.blue); |
| scale= 2 - scale; |
| } |
| } |
| |
| return Colors.blend(background, color, scale); |
| } |
| |
| private long computeAge(Revision revision) { |
| return revision.getDate().getTime(); |
| } |
| |
| /** |
| * Returns the color for a revision based on relative age and author. |
| * |
| * @param revision the revision |
| * @param focus <code>true</code> to return the focus color |
| * @return the color for a revision |
| */ |
| public RGB getColor(Revision revision, boolean focus) { |
| Map<Revision, RGB> map= focus ? fFocusColors : fColors; |
| RGB color= map.get(revision); |
| if (color != null) |
| return color; |
| |
| color= adaptColor(revision, focus); |
| map.put(revision, color); |
| return color; |
| } |
| } |
| |
| /** |
| * Handles all the mouse interaction in this line number ruler column. |
| */ |
| private class MouseHandler implements MouseMoveListener, MouseTrackListener, Listener { |
| |
| private RevisionRange fMouseDownRegion; |
| |
| private void handleMouseUp(Event e) { |
| if (e.button == 1) { |
| RevisionRange upRegion= fFocusRange; |
| RevisionRange downRegion= fMouseDownRegion; |
| fMouseDownRegion= null; |
| |
| if (upRegion == downRegion) { |
| Revision revision= upRegion == null ? null : upRegion.getRevision(); |
| if (revision == fSelectedRevision) |
| revision= null; // deselect already selected revision |
| handleRevisionSelected(revision); |
| } |
| } |
| } |
| |
| private void handleMouseDown(Event e) { |
| if (e.button == 3) |
| updateFocusRevision(null); // kill any focus as the ctx menu is going to show |
| if (e.button == 1) { |
| fMouseDownRegion= fFocusRange; |
| postRedraw(); |
| } |
| } |
| |
| @Override |
| public void handleEvent(Event event) { |
| switch (event.type) { |
| case SWT.MouseVerticalWheel: |
| handleMouseWheel(event); |
| break; |
| case SWT.MouseDown: |
| handleMouseDown(event); |
| break; |
| case SWT.MouseUp: |
| handleMouseUp(event); |
| break; |
| default: |
| Assert.isLegal(false); |
| } |
| } |
| |
| @Override |
| public void mouseEnter(MouseEvent e) { |
| updateFocusLine(toDocumentLineNumber(e.y)); |
| } |
| |
| @Override |
| public void mouseExit(MouseEvent e) { |
| updateFocusLine(-1); |
| } |
| |
| @Override |
| public void mouseHover(MouseEvent e) { |
| } |
| |
| @Override |
| public void mouseMove(MouseEvent e) { |
| updateFocusLine(toDocumentLineNumber(e.y)); |
| } |
| } |
| |
| /** |
| * Internal listener class that will update the ruler when the underlying model changes. |
| */ |
| private class AnnotationListener implements IAnnotationModelListener { |
| @Override |
| public void modelChanged(IAnnotationModel model) { |
| clearRangeCache(); |
| postRedraw(); |
| } |
| |
| } |
| |
| /** |
| * The information control creator. |
| */ |
| private static final class HoverInformationControlCreator extends AbstractReusableInformationControlCreator { |
| private boolean fIsFocusable; |
| |
| public HoverInformationControlCreator(boolean isFocusable) { |
| fIsFocusable= isFocusable; |
| } |
| |
| @Override |
| protected IInformationControl doCreateInformationControl(Shell parent) { |
| if (BrowserInformationControl.isAvailable(parent)) { |
| return new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, fIsFocusable) { |
| /** |
| * {@inheritDoc} |
| * |
| * @deprecated use {@link #setInput(Object)} |
| */ |
| @Deprecated |
| @Override |
| public void setInformation(String content) { |
| content= addCSSToHTMLFragment(content); |
| super.setInformation(content); |
| } |
| |
| /** |
| * Adds a HTML header and CSS info if <code>html</code> is only an HTML fragment (has no |
| * <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("<html>") != -1) //$NON-NLS-1$ |
| // there is already a header |
| return html; |
| |
| StringBuilder info= new StringBuilder(512 + html.length()); |
| HTMLPrinter.insertPageProlog(info, 0, fgStyleSheet); |
| info.append(html); |
| HTMLPrinter.addPageEpilog(info); |
| return info.toString(); |
| } |
| |
| }; |
| } |
| return new DefaultInformationControl(parent, fIsFocusable); |
| } |
| |
| @Override |
| public boolean canReplace(IInformationControlCreator creator) { |
| return creator.getClass() == getClass() |
| && ((HoverInformationControlCreator) creator).fIsFocusable == fIsFocusable; |
| } |
| } |
| |
| private static final String fgStyleSheet= "/* Font definitions */\n" + //$NON-NLS-1$ |
| "body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt {font-family: sans-serif; font-size: 9pt }\n" + //$NON-NLS-1$ |
| "pre { font-family: monospace; font-size: 9pt }\n" + //$NON-NLS-1$ |
| "\n" + //$NON-NLS-1$ |
| "/* Margins */\n" + //$NON-NLS-1$ |
| "body { overflow: auto; margin-top: 0; margin-bottom: 4; margin-left: 3; margin-right: 0 }\n" + //$NON-NLS-1$ |
| "h1 { margin-top: 5; margin-bottom: 1 } \n" + //$NON-NLS-1$ |
| "h2 { margin-top: 25; margin-bottom: 3 }\n" + //$NON-NLS-1$ |
| "h3 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$ |
| "h4 { margin-top: 20; margin-bottom: 3 }\n" + //$NON-NLS-1$ |
| "h5 { margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$ |
| "p { margin-top: 10px; margin-bottom: 10px }\n" + //$NON-NLS-1$ |
| "pre { margin-left: 6 }\n" + //$NON-NLS-1$ |
| "ul { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ |
| "li { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$ |
| "li p { margin-top: 0; margin-bottom: 0 } \n" + //$NON-NLS-1$ |
| "ol { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ |
| "dl { margin-top: 0; margin-bottom: 10 }\n" + //$NON-NLS-1$ |
| "dt { margin-top: 0; margin-bottom: 0; font-weight: bold }\n" + //$NON-NLS-1$ |
| "dd { margin-top: 0; margin-bottom: 0 }\n" + //$NON-NLS-1$ |
| "\n" + //$NON-NLS-1$ |
| "/* Styles and colors */\n" + //$NON-NLS-1$ |
| "a:link { color: hyperlinkColor }\n" + //$NON-NLS-1$ |
| "a:hover { color: activeHyperlinkColor; }\n" + //$NON-NLS-1$ |
| "a:visited { text-decoration: underline }\n" + //$NON-NLS-1$ |
| "h4 { font-style: italic }\n" + //$NON-NLS-1$ |
| "strong { font-weight: bold }\n" + //$NON-NLS-1$ |
| "em { font-style: italic }\n" + //$NON-NLS-1$ |
| "var { font-style: italic }\n" + //$NON-NLS-1$ |
| "th { font-weight: bold }\n" + //$NON-NLS-1$ |
| ""; //$NON-NLS-1$ |
| |
| /** |
| * The revision hover displays information about the currently selected revision. |
| */ |
| private final class RevisionHover implements IAnnotationHover, IAnnotationHoverExtension, IAnnotationHoverExtension2, IInformationProviderExtension2 { |
| |
| @Override |
| public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) { |
| Object info= getHoverInfo(sourceViewer, getHoverLineRange(sourceViewer, lineNumber), 0); |
| return info == null ? null : info.toString(); |
| } |
| |
| @Override |
| public IInformationControlCreator getHoverControlCreator() { |
| RevisionInformation revisionInfo= fRevisionInfo; |
| if (revisionInfo != null) { |
| IInformationControlCreator creator= revisionInfo.getHoverControlCreator(); |
| if (creator != null) |
| return creator; |
| } |
| return new HoverInformationControlCreator(false); |
| } |
| |
| @Override |
| public boolean canHandleMouseCursor() { |
| return false; |
| } |
| |
| @Override |
| public boolean canHandleMouseWheel() { |
| return true; |
| } |
| |
| @Override |
| public Object getHoverInfo(ISourceViewer sourceViewer, ILineRange lineRange, int visibleNumberOfLines) { |
| RevisionRange range= getRange(lineRange.getStartLine()); |
| Object info= range == null ? null : range.getRevision().getHoverInfo(); |
| return info; |
| } |
| |
| @Override |
| public ILineRange getHoverLineRange(ISourceViewer viewer, int lineNumber) { |
| RevisionRange range= getRange(lineNumber); |
| return range == null ? null : new LineRange(lineNumber, 1); |
| } |
| |
| @Override |
| public IInformationControlCreator getInformationPresenterControlCreator() { |
| RevisionInformation revisionInfo= fRevisionInfo; |
| if (revisionInfo != null) { |
| IInformationControlCreator creator= revisionInfo.getInformationPresenterControlCreator(); |
| if (creator != null) |
| return creator; |
| } |
| return new HoverInformationControlCreator(true); |
| } |
| } |
| |
| /* Listeners and helpers. */ |
| |
| /** The shared color provider. */ |
| private final ISharedTextColors fSharedColors; |
| /** The color tool. */ |
| private final ColorTool fColorTool= new ColorTool(); |
| /** The mouse handler. */ |
| private final MouseHandler fMouseHandler= new MouseHandler(); |
| /** The hover. */ |
| private final RevisionHover fHover= new RevisionHover(); |
| /** The annotation listener. */ |
| private final AnnotationListener fAnnotationListener= new AnnotationListener(); |
| /** The selection provider. */ |
| private final RevisionSelectionProvider fRevisionSelectionProvider= new RevisionSelectionProvider(this); |
| /** |
| * The list of revision listeners. |
| * @since 3.3. |
| */ |
| private final ListenerList<IRevisionListener> fRevisionListeners= new ListenerList<>(ListenerList.IDENTITY); |
| |
| /* The context - column and viewer we are connected to. */ |
| |
| /** The vertical ruler column that delegates painting to this painter. */ |
| private final IVerticalRulerColumn fColumn; |
| /** The parent ruler. */ |
| private CompositeRuler fParentRuler; |
| /** The column's control, typically a {@link Canvas}, possibly <code>null</code>. */ |
| private Control fControl; |
| /** The text viewer that the column is attached to. */ |
| private ITextViewer fViewer; |
| /** The viewer's text widget. */ |
| private StyledText fWidget; |
| |
| /* The models we operate on. */ |
| |
| /** The revision model object. */ |
| private RevisionInformation fRevisionInfo; |
| /** The line differ. */ |
| private ILineDiffer fLineDiffer= null; |
| /** The annotation model. */ |
| private IAnnotationModel fAnnotationModel= null; |
| /** The background color, possibly <code>null</code>. */ |
| private Color fBackground; |
| |
| /* Cache. */ |
| |
| /** The cached list of ranges adapted to quick diff. */ |
| private List<RevisionRange> fRevisionRanges= null; |
| /** The annotations created for the overview ruler temporary display. */ |
| private List<Annotation> fAnnotations= new ArrayList<>(); |
| |
| /* State */ |
| |
| /** The current focus line, -1 for none. */ |
| private int fFocusLine= -1; |
| /** The current focus region, <code>null</code> if none. */ |
| private RevisionRange fFocusRange= null; |
| /** The current focus revision, <code>null</code> if none. */ |
| private Revision fFocusRevision= null; |
| /** |
| * The currently selected revision, <code>null</code> if none. The difference between |
| * {@link #fFocusRevision} and {@link #fSelectedRevision} may not be obvious: the focus revision |
| * is the one focused by the mouse (by hovering over a block of the revision), while the |
| * selected revision is sticky, i.e. is not removed when the mouse leaves the ruler. |
| * |
| * @since 3.3 |
| */ |
| private Revision fSelectedRevision= null; |
| /** <code>true</code> if the mouse wheel handler is installed, <code>false</code> otherwise. */ |
| private boolean fWheelHandlerInstalled= false; |
| /** |
| * The revision rendering mode. |
| */ |
| private RenderingMode fRenderingMode= IRevisionRulerColumnExtension.AUTHOR_SHADED_BY_AGE; |
| /** |
| * The required with in characters. |
| * @since 3.3 |
| */ |
| private int fRequiredWidth= -1; |
| /** |
| * The width of the revision field in chars to compute {@link #fAuthorInset} from. |
| * @since 3.3 |
| */ |
| private int fRevisionIdChars= 0; |
| /** |
| * <code>true</code> to show revision ids, <code>false</code> otherwise. |
| * @since 3.3 |
| */ |
| private boolean fShowRevision= false; |
| /** |
| * <code>true</code> to show the author, <code>false</code> otherwise. |
| * @since 3.3 |
| */ |
| private boolean fShowAuthor= false; |
| /** |
| * The author inset in pixels for when author *and* revision id are shown. |
| * @since 3.3 |
| */ |
| private int fAuthorInset; |
| /** |
| * The remembered ruler width (as changing the ruler width triggers recomputation of the colors. |
| * @since 3.3 |
| */ |
| private int fLastWidth= -1; |
| /** |
| * The zoom level for the current painting operation. Workaround for bug 516293. |
| * @since 3.12 |
| */ |
| private int fZoom= 100; |
| |
| /** |
| * Creates a new revision painter for a vertical ruler column. |
| * |
| * @param column the column that will delegate{@link #paint(GC, ILineRange) painting} to the |
| * newly created painter. |
| * @param sharedColors a shared colors object to store shaded colors in |
| */ |
| public RevisionPainter(IVerticalRulerColumn column, ISharedTextColors sharedColors) { |
| Assert.isLegal(column != null); |
| Assert.isLegal(sharedColors != null); |
| fColumn= column; |
| fSharedColors= sharedColors; |
| } |
| |
| /** |
| * Sets the revision information to be drawn and triggers a redraw. |
| * |
| * @param info the revision information to show, <code>null</code> to draw none |
| */ |
| public void setRevisionInformation(RevisionInformation info) { |
| if (fRevisionInfo != info) { |
| fRequiredWidth= -1; |
| fRevisionIdChars= 0; |
| fRevisionInfo= info; |
| clearRangeCache(); |
| updateFocusRange(null); |
| handleRevisionSelected((Revision) null); |
| fColorTool.setInfo(info); |
| postRedraw(); |
| informListeners(); |
| } |
| } |
| |
| /** |
| * Changes the rendering mode and triggers redrawing if needed. |
| * |
| * @param renderingMode the rendering mode |
| * @since 3.3 |
| */ |
| public void setRenderingMode(RenderingMode renderingMode) { |
| Assert.isLegal(renderingMode != null); |
| if (fRenderingMode != renderingMode) { |
| fRenderingMode= renderingMode; |
| fColorTool.setInfo(fRevisionInfo); |
| postRedraw(); |
| } |
| } |
| |
| /** |
| * Sets the background color. |
| * |
| * @param background the background color, <code>null</code> for the platform's list |
| * background |
| */ |
| public void setBackground(Color background) { |
| fBackground= background; |
| } |
| |
| /** |
| * Sets the parent ruler - the delegating column must call this method as soon as it creates its |
| * control. |
| * |
| * @param parentRuler the parent ruler |
| */ |
| public void setParentRuler(CompositeRuler parentRuler) { |
| fParentRuler= parentRuler; |
| } |
| |
| /** |
| * Sets the zoom level for the current painting operation. Workaround for bug 516293. |
| * |
| * @param zoom the zoom to set |
| * @since 3.12 |
| */ |
| public void setZoom(int zoom) { |
| fZoom= zoom; |
| } |
| |
| private int autoScaleUp(int value) { |
| return value * fZoom / 100; |
| } |
| |
| /** |
| * Delegates the painting of the quick diff colors to this painter. The painter will draw the |
| * color boxes onto the passed {@link GC} for all model (document) lines in |
| * <code>visibleModelLines</code>. |
| * |
| * @param gc the {@link GC} to draw onto |
| * @param visibleLines the lines (in document offsets) that are currently (perhaps only |
| * partially) visible |
| */ |
| public void paint(GC gc, ILineRange visibleLines) { |
| connectIfNeeded(); |
| if (!isConnected()) |
| return; |
| |
| // compute the horizontal indent of the author for the case that we show revision |
| // and author |
| if (fShowAuthor && fShowRevision) { |
| char[] string= new char[fRevisionIdChars + 1]; |
| Arrays.fill(string, '9'); |
| if (string.length > 1) { |
| string[0]= '.'; |
| string[1]= ' '; |
| } |
| fAuthorInset= gc.stringExtent(new String(string)).x; |
| } |
| |
| // recompute colors (show intense colors if ruler is narrow) |
| int width= getWidth(); |
| if (width != fLastWidth) { |
| fColorTool.setInfo(fRevisionInfo); |
| fLastWidth= width; |
| } |
| |
| // draw change regions |
| List<RevisionRange> ranges= getRanges(visibleLines); |
| for (RevisionRange region : ranges) { |
| paintRange(region, gc); |
| } |
| } |
| |
| /** |
| * Ensures that the column is fully instantiated, i.e. has a control, and that the viewer is |
| * visible. |
| */ |
| private void connectIfNeeded() { |
| if (isConnected() || fParentRuler == null) |
| return; |
| |
| fViewer= fParentRuler.getTextViewer(); |
| if (fViewer == null) |
| return; |
| |
| fWidget= fViewer.getTextWidget(); |
| if (fWidget == null) |
| return; |
| |
| fControl= fColumn.getControl(); |
| if (fControl == null) |
| return; |
| |
| fControl.addMouseTrackListener(fMouseHandler); |
| fControl.addMouseMoveListener(fMouseHandler); |
| fControl.addListener(SWT.MouseUp, fMouseHandler); |
| fControl.addListener(SWT.MouseDown, fMouseHandler); |
| fControl.addDisposeListener(e -> handleDispose()); |
| |
| fRevisionSelectionProvider.install(fViewer); |
| } |
| |
| /** |
| * Returns <code>true</code> if the column is fully connected. |
| * |
| * @return <code>true</code> if the column is fully connected, false otherwise |
| */ |
| private boolean isConnected() { |
| return fControl != null; |
| } |
| |
| /** |
| * Sets the annotation model. |
| * |
| * @param model the annotation model, possibly <code>null</code> |
| * @see IVerticalRulerColumn#setModel(IAnnotationModel) |
| */ |
| public void setModel(IAnnotationModel model) { |
| IAnnotationModel diffModel; |
| if (model instanceof IAnnotationModelExtension) |
| diffModel= ((IAnnotationModelExtension) model).getAnnotationModel(IChangeRulerColumn.QUICK_DIFF_MODEL_ID); |
| else |
| diffModel= model; |
| |
| setDiffer(diffModel); |
| setAnnotationModel(model); |
| } |
| |
| /** |
| * Sets the annotation model. |
| * |
| * @param model the annotation model. |
| */ |
| private void setAnnotationModel(IAnnotationModel model) { |
| if (fAnnotationModel != model) |
| fAnnotationModel= model; |
| } |
| |
| /** |
| * Sets the line differ. |
| * |
| * @param differ the line differ or <code>null</code> if none |
| */ |
| private void setDiffer(IAnnotationModel differ) { |
| if (differ instanceof ILineDiffer || differ == null) { |
| if (fLineDiffer != differ) { |
| if (fLineDiffer != null) |
| ((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener); |
| fLineDiffer= (ILineDiffer) differ; |
| if (fLineDiffer != null) |
| ((IAnnotationModel) fLineDiffer).addAnnotationModelListener(fAnnotationListener); |
| } |
| } |
| } |
| |
| /** |
| * Disposes of the painter's resources. |
| */ |
| private void handleDispose() { |
| updateFocusLine(-1); |
| |
| if (fLineDiffer != null) { |
| ((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener); |
| fLineDiffer= null; |
| } |
| |
| fRevisionSelectionProvider.uninstall(); |
| } |
| |
| /** |
| * Paints a single change region onto <code>gc</code>. |
| * |
| * @param range the range to paint |
| * @param gc the {@link GC} to paint on |
| */ |
| private void paintRange(RevisionRange range, GC gc) { |
| ILineRange widgetRange= modelLinesToWidgetLines(range); |
| if (widgetRange == null) |
| return; |
| |
| Revision revision= range.getRevision(); |
| boolean drawArmedFocus= range == fMouseHandler.fMouseDownRegion; |
| boolean drawSelection= !drawArmedFocus && revision == fSelectedRevision; |
| boolean drawFocus= !drawSelection && !drawArmedFocus && revision == fFocusRevision; |
| Rectangle box= computeBoxBounds(widgetRange); |
| |
| gc.setBackground(lookupColor(revision, false)); |
| if (drawArmedFocus) { |
| Color foreground= gc.getForeground(); |
| Color focusColor= lookupColor(revision, true); |
| gc.setForeground(focusColor); |
| gc.fillRectangle(box); |
| gc.drawRectangle(box.x, box.y, box.width - 1, box.height - 1); // highlight box |
| gc.drawRectangle(box.x + 1, box.y + 1, box.width - 3, box.height - 3); // inner highlight box |
| gc.setForeground(foreground); |
| } else if (drawFocus || drawSelection) { |
| Color foreground= gc.getForeground(); |
| Color focusColor= lookupColor(revision, true); |
| gc.setForeground(focusColor); |
| gc.fillRectangle(box); |
| gc.drawRectangle(box.x, box.y, box.width - 1, box.height - 1); // highlight box |
| gc.setForeground(foreground); |
| } else { |
| gc.fillRectangle(box); |
| } |
| |
| if ((fShowAuthor || fShowRevision)) { |
| int indentation= 1; |
| int baselineBias= getBaselineBias(gc, widgetRange.getStartLine()); |
| if (fShowAuthor && fShowRevision) { |
| gc.drawString(revision.getId(), indentation, box.y + baselineBias, true); |
| gc.drawString(revision.getAuthor(), fAuthorInset, box.y + baselineBias, true); |
| } else if (fShowAuthor) { |
| gc.drawString(revision.getAuthor(), indentation, box.y + baselineBias, true); |
| } else if (fShowRevision) { |
| gc.drawString(revision.getId(), indentation, box.y + baselineBias, true); |
| } |
| } |
| } |
| |
| /** |
| * Returns the difference between the baseline of the widget and the |
| * baseline as specified by the font for <code>gc</code>. When drawing |
| * line numbers, the returned bias should be added to obtain text lined up |
| * on the correct base line of the text widget. |
| * |
| * @param gc the <code>GC</code> to get the font metrics from |
| * @param widgetLine the widget line |
| * @return the baseline bias to use when drawing text that is lined up with |
| * <code>fCachedTextWidget</code> |
| * @since 3.3 |
| */ |
| private int getBaselineBias(GC gc, int widgetLine) { |
| if (widgetLine == fWidget.getLineCount()) |
| widgetLine--; |
| |
| /* |
| * https://bugs.eclipse.org/bugs/show_bug.cgi?id=62951 |
| * widget line height may be more than the font height used for the |
| * line numbers, since font styles (bold, italics...) can have larger |
| * font metrics than the simple font used for the numbers. |
| */ |
| int offset= fWidget.getOffsetAtLine(widgetLine); |
| int widgetBaseline= fWidget.getBaseline(offset); |
| |
| FontMetrics fm = gc.getFontMetrics(); |
| int fontBaseline = fm.getAscent() + fm.getLeading(); |
| int baselineBias= widgetBaseline - fontBaseline; |
| return Math.max(0, baselineBias); |
| } |
| |
| /** |
| * Looks up the color for a certain revision. |
| * |
| * @param revision the revision to get the color for |
| * @param focus <code>true</code> if it is the focus revision |
| * @return the color for the revision |
| */ |
| private Color lookupColor(Revision revision, boolean focus) { |
| return fSharedColors.getColor(fColorTool.getColor(revision, focus)); |
| } |
| |
| /** |
| * Returns the revision range that contains the given line, or |
| * <code>null</code> if there is none. |
| * |
| * @param line the line of interest |
| * @return the corresponding <code>RevisionRange</code> or <code>null</code> |
| */ |
| private RevisionRange getRange(int line) { |
| List<RevisionRange> ranges= getRangeCache(); |
| |
| if (ranges.isEmpty() || line == -1) |
| return null; |
| |
| for (RevisionRange range : ranges) { |
| if (contains(range, line)) |
| return range; |
| } |
| |
| // line may be right after the last region |
| RevisionRange lastRegion= ranges.get(ranges.size() - 1); |
| if (line == end(lastRegion)) |
| return lastRegion; |
| return null; |
| } |
| |
| /** |
| * Returns the sublist of all <code>RevisionRange</code>s that intersect with the given lines. |
| * |
| * @param lines the model based lines of interest |
| * @return elementType: RevisionRange |
| */ |
| private List<RevisionRange> getRanges(ILineRange lines) { |
| List<RevisionRange> ranges= getRangeCache(); |
| |
| // return the interesting subset |
| int end= end(lines); |
| int first= -1, last= -1; |
| for (int i= 0; i < ranges.size(); i++) { |
| RevisionRange range= ranges.get(i); |
| int rangeEnd= end(range); |
| if (first == -1 && rangeEnd > lines.getStartLine()) |
| first= i; |
| if (first != -1 && rangeEnd > end) { |
| last= i; |
| break; |
| } |
| } |
| if (first == -1) |
| return Collections.emptyList(); |
| if (last == -1) |
| last= ranges.size() - 1; // bottom index may be one too much |
| |
| return ranges.subList(first, last + 1); |
| } |
| |
| /** |
| * Gets all change ranges of the revisions in the revision model and adapts them to the current |
| * quick diff information. The list is cached. |
| * |
| * @return the list of all change regions, with diff information applied |
| */ |
| private synchronized List<RevisionRange> getRangeCache() { |
| if (fRevisionRanges == null) { |
| if (fRevisionInfo == null) { |
| fRevisionRanges= Collections.emptyList(); |
| } else { |
| Hunk[] hunks= HunkComputer.computeHunks(fLineDiffer, fViewer.getDocument().getNumberOfLines()); |
| fRevisionInfo.applyDiff(hunks); |
| fRevisionRanges= fRevisionInfo.getRanges(); |
| updateOverviewAnnotations(); |
| informListeners(); |
| } |
| } |
| |
| return fRevisionRanges; |
| } |
| |
| /** |
| * Clears the range cache. |
| * |
| * @since 3.3 |
| */ |
| private synchronized void clearRangeCache() { |
| fRevisionRanges= null; |
| } |
| |
| /** |
| * Returns <code>true</code> if <code>range</code> contains <code>line</code>. A line is |
| * not contained in a range if it is the range's exclusive end line. |
| * |
| * @param range the range to check whether it contains <code>line</code> |
| * @param line the line the line |
| * @return <code>true</code> if <code>range</code> contains <code>line</code>, |
| * <code>false</code> if not |
| */ |
| private static boolean contains(ILineRange range, int line) { |
| return range.getStartLine() <= line && end(range) > line; |
| } |
| |
| /** |
| * Computes the end index of a line range. |
| * |
| * @param range a line range |
| * @return the last line (exclusive) of <code>range</code> |
| */ |
| private static int end(ILineRange range) { |
| return range.getStartLine() + range.getNumberOfLines(); |
| } |
| |
| /** |
| * Returns the visible extent of a document line range in widget lines. |
| * |
| * @param range the document line range |
| * @return the visible extent of <code>range</code> in widget lines |
| */ |
| private ILineRange modelLinesToWidgetLines(ILineRange range) { |
| int widgetStartLine= -1; |
| int widgetEndLine= -1; |
| if (fViewer instanceof ITextViewerExtension5) { |
| ITextViewerExtension5 extension= (ITextViewerExtension5) fViewer; |
| int modelEndLine= end(range); |
| for (int modelLine= range.getStartLine(); modelLine < modelEndLine; modelLine++) { |
| int widgetLine= extension.modelLine2WidgetLine(modelLine); |
| if (widgetLine != -1) { |
| if (widgetStartLine == -1) |
| widgetStartLine= widgetLine; |
| widgetEndLine= widgetLine; |
| } |
| } |
| } else { |
| IRegion region= fViewer.getVisibleRegion(); |
| IDocument document= fViewer.getDocument(); |
| try { |
| int visibleStartLine= document.getLineOfOffset(region.getOffset()); |
| int visibleEndLine= document.getLineOfOffset(region.getOffset() + region.getLength()); |
| widgetStartLine= Math.max(0, range.getStartLine() - visibleStartLine); |
| widgetEndLine= Math.min(visibleEndLine, end(range) - 1); |
| } catch (BadLocationException x) { |
| // ignore and return null |
| } |
| } |
| if (widgetStartLine == -1 || widgetEndLine == -1) |
| return null; |
| return new LineRange(widgetStartLine, widgetEndLine - widgetStartLine + 1); |
| } |
| |
| /** |
| * Returns the revision hover. |
| * |
| * @return the revision hover |
| */ |
| public IAnnotationHover getHover() { |
| return fHover; |
| } |
| |
| /** |
| * Computes and returns the bounds of the rectangle corresponding to a widget line range. The |
| * rectangle is in pixel coordinates relative to the text widget's |
| * {@link StyledText#getClientArea() client area} and has the width of the ruler. |
| * |
| * @param range the widget line range |
| * @return the box bounds corresponding to <code>range</code> |
| */ |
| private Rectangle computeBoxBounds(ILineRange range) { |
| int y1= fWidget.getLinePixel(range.getStartLine()); |
| int y2= fWidget.getLinePixel(range.getStartLine() + range.getNumberOfLines()); |
| |
| return new Rectangle(0, autoScaleUp(y1), autoScaleUp(getWidth()), autoScaleUp(y2 - y1 - 1)); |
| } |
| |
| /** |
| * Shows (or hides) the overview annotations. |
| */ |
| private void updateOverviewAnnotations() { |
| if (fAnnotationModel == null) |
| return; |
| |
| Revision revision= fFocusRevision != null ? fFocusRevision : fSelectedRevision; |
| |
| Map<Annotation, Position> added= null; |
| if (revision != null) { |
| added= new HashMap<>(); |
| for (RevisionRange range : revision.getRegions()) { |
| try { |
| IRegion charRegion= toCharRegion(range); |
| Position position= new Position(charRegion.getOffset(), charRegion.getLength()); |
| Annotation annotation= new RevisionAnnotation(revision.getId()); |
| added.put(annotation, position); |
| } catch (BadLocationException x) { |
| // ignore - document was changed, show no annotations |
| } |
| } |
| } |
| |
| if (fAnnotationModel instanceof IAnnotationModelExtension) { |
| IAnnotationModelExtension ext= (IAnnotationModelExtension) fAnnotationModel; |
| ext.replaceAnnotations(fAnnotations.toArray(new Annotation[fAnnotations.size()]), added); |
| } else { |
| for (Annotation annotation : fAnnotations) { |
| fAnnotationModel.removeAnnotation(annotation); |
| } |
| if (added != null) { |
| for (Entry<Annotation, Position> entry : added.entrySet()) { |
| fAnnotationModel.addAnnotation(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| fAnnotations.clear(); |
| if (added != null) |
| fAnnotations.addAll(added.keySet()); |
| |
| } |
| |
| /** |
| * Returns the character offset based region of a line range. |
| * |
| * @param lines the line range to convert |
| * @return the character offset range corresponding to <code>range</code> |
| * @throws BadLocationException if the line range is not within the document bounds |
| */ |
| private IRegion toCharRegion(ILineRange lines) throws BadLocationException { |
| IDocument document= fViewer.getDocument(); |
| int offset= document.getLineOffset(lines.getStartLine()); |
| int nextLine= end(lines); |
| int endOffset; |
| if (nextLine >= document.getNumberOfLines()) |
| endOffset= document.getLength(); |
| else |
| endOffset= document.getLineOffset(nextLine); |
| return new Region(offset, endOffset - offset); |
| } |
| |
| /** |
| * Handles the selection of a revision and informs listeners. |
| * |
| * @param revision the selected revision, <code>null</code> for none |
| */ |
| void handleRevisionSelected(Revision revision) { |
| fSelectedRevision= revision; |
| fRevisionSelectionProvider.revisionSelected(revision); |
| |
| if (isConnected()) |
| updateOverviewAnnotations(); |
| |
| postRedraw(); |
| } |
| |
| /** |
| * Handles the selection of a revision id and informs listeners |
| * |
| * @param id the selected revision id |
| */ |
| void handleRevisionSelected(String id) { |
| Assert.isLegal(id != null); |
| if (fRevisionInfo == null) |
| return; |
| |
| for (Revision revision : fRevisionInfo.getRevisions()) { |
| if (id.equals(revision.getId())) { |
| handleRevisionSelected(revision); |
| return; |
| } |
| } |
| |
| // clear selection if it does not exist |
| handleRevisionSelected((Revision) null); |
| } |
| |
| /** |
| * Returns the selection provider. |
| * |
| * @return the selection provider |
| */ |
| public RevisionSelectionProvider getRevisionSelectionProvider() { |
| return fRevisionSelectionProvider; |
| } |
| |
| /** |
| * Updates the focus line with a new line. |
| * |
| * @param line the new focus line, -1 for no focus |
| */ |
| private void updateFocusLine(int line) { |
| if (fFocusLine != line) |
| onFocusLineChanged(fFocusLine, line); |
| } |
| |
| /** |
| * Handles a changing focus line. |
| * |
| * @param previousLine the old focus line (-1 for no focus) |
| * @param nextLine the new focus line (-1 for no focus) |
| */ |
| private void onFocusLineChanged(int previousLine, int nextLine) { |
| if (DEBUG) |
| System.out.println("line: " + previousLine + " > " + nextLine); //$NON-NLS-1$ //$NON-NLS-2$ |
| fFocusLine= nextLine; |
| RevisionRange region= getRange(nextLine); |
| updateFocusRange(region); |
| } |
| |
| /** |
| * Updates the focus range. |
| * |
| * @param range the new focus range, <code>null</code> for no focus |
| */ |
| private void updateFocusRange(RevisionRange range) { |
| if (range != fFocusRange) |
| onFocusRangeChanged(fFocusRange, range); |
| } |
| |
| /** |
| * Handles a changing focus range. |
| * |
| * @param previousRange the old focus range (<code>null</code> for no focus) |
| * @param nextRange the new focus range (<code>null</code> for no focus) |
| */ |
| private void onFocusRangeChanged(RevisionRange previousRange, RevisionRange nextRange) { |
| if (DEBUG) |
| System.out.println("range: " + previousRange + " > " + nextRange); //$NON-NLS-1$ //$NON-NLS-2$ |
| fFocusRange= nextRange; |
| Revision revision= nextRange == null ? null : nextRange.getRevision(); |
| updateFocusRevision(revision); |
| } |
| |
| private void updateFocusRevision(Revision revision) { |
| if (fFocusRevision != revision) |
| onFocusRevisionChanged(fFocusRevision, revision); |
| } |
| |
| /** |
| * Handles a changing focus revision. |
| * |
| * @param previousRevision the old focus revision (<code>null</code> for no focus) |
| * @param nextRevision the new focus revision (<code>null</code> for no focus) |
| */ |
| private void onFocusRevisionChanged(Revision previousRevision, Revision nextRevision) { |
| if (DEBUG) |
| System.out.println("revision: " + previousRevision + " > " + nextRevision); //$NON-NLS-1$ //$NON-NLS-2$ |
| fFocusRevision= nextRevision; |
| uninstallWheelHandler(); |
| installWheelHandler(); |
| updateOverviewAnnotations(); |
| redraw(); // pick up new highlights |
| } |
| |
| /** |
| * Uninstalls the mouse wheel handler. |
| */ |
| private void uninstallWheelHandler() { |
| fControl.removeListener(SWT.MouseVerticalWheel, fMouseHandler); |
| fWheelHandlerInstalled= false; |
| } |
| |
| /** |
| * Installs the mouse wheel handler. |
| */ |
| private void installWheelHandler() { |
| if (fFocusRevision != null && !fWheelHandlerInstalled) { |
| //FIXME: does not work on Windows, because Canvas cannot get focus and therefore does not send out mouse wheel events: |
| //https://bugs.eclipse.org/bugs/show_bug.cgi?id=81189 |
| //see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=75766 |
| fControl.addListener(SWT.MouseVerticalWheel, fMouseHandler); |
| fWheelHandlerInstalled= true; |
| } |
| } |
| |
| /** |
| * @return <code>true</code> iff the mouse wheel handler is installed and others should avoid |
| * handling mouse wheel events |
| * @since 3.10 |
| */ |
| public boolean isWheelHandlerInstalled() { |
| return fWheelHandlerInstalled; |
| } |
| |
| /** |
| * Handles a mouse wheel event. |
| * |
| * @param event the mouse wheel event |
| */ |
| private void handleMouseWheel(Event event) { |
| boolean up= event.count > 0; |
| int documentHoverLine= fFocusLine; |
| |
| ILineRange nextWidgetRange= null; |
| ILineRange last= null; |
| List<RevisionRange> ranges= fFocusRevision.getRegions(); |
| if (up) { |
| for (RevisionRange range : ranges) { |
| ILineRange widgetRange= modelLinesToWidgetLines(range); |
| if (contains(range, documentHoverLine)) { |
| nextWidgetRange= last; |
| break; |
| } |
| if (widgetRange != null) |
| last= widgetRange; |
| } |
| } else { |
| for (ListIterator<RevisionRange> it= ranges.listIterator(ranges.size()); it.hasPrevious();) { |
| RevisionRange range= it.previous(); |
| ILineRange widgetRange= modelLinesToWidgetLines(range); |
| if (contains(range, documentHoverLine)) { |
| nextWidgetRange= last; |
| break; |
| } |
| if (widgetRange != null) |
| last= widgetRange; |
| } |
| } |
| |
| if (nextWidgetRange == null) |
| return; |
| |
| int widgetCurrentFocusLine= modelLinesToWidgetLines(new LineRange(documentHoverLine, 1)).getStartLine(); |
| int widgetNextFocusLine= nextWidgetRange.getStartLine(); |
| int newTopPixel= fWidget.getTopPixel() + JFaceTextUtil.computeLineHeight(fWidget, widgetCurrentFocusLine, widgetNextFocusLine, widgetNextFocusLine - widgetCurrentFocusLine); |
| fWidget.setTopPixel(newTopPixel); |
| if (newTopPixel < 0) { |
| Point cursorLocation= fWidget.getDisplay().getCursorLocation(); |
| cursorLocation.y+= newTopPixel; |
| fWidget.getDisplay().setCursorLocation(cursorLocation); |
| } else { |
| int topPixel= fWidget.getTopPixel(); |
| if (topPixel < newTopPixel) { |
| Point cursorLocation= fWidget.getDisplay().getCursorLocation(); |
| cursorLocation.y+= newTopPixel - topPixel; |
| fWidget.getDisplay().setCursorLocation(cursorLocation); |
| } |
| } |
| updateFocusLine(toDocumentLineNumber(fWidget.toControl(fWidget.getDisplay().getCursorLocation()).y)); |
| immediateUpdate(); |
| } |
| |
| /** |
| * Triggers a redraw in the display thread. |
| */ |
| private final void postRedraw() { |
| if (isConnected() && !fControl.isDisposed()) { |
| Display d= fControl.getDisplay(); |
| if (d != null) { |
| d.asyncExec(() -> redraw()); |
| } |
| } |
| } |
| |
| /** |
| * Translates a y coordinate in the pixel coordinates of the column's control to a document line |
| * number. |
| * |
| * @param y the y coordinate |
| * @return the corresponding document line, -1 for no line |
| * @see CompositeRuler#toDocumentLineNumber(int) |
| */ |
| private int toDocumentLineNumber(int y) { |
| return fParentRuler.toDocumentLineNumber(y); |
| } |
| |
| /** |
| * Triggers redrawing of the column. |
| */ |
| private void redraw() { |
| fColumn.redraw(); |
| } |
| |
| /** |
| * Triggers immediate redrawing of the entire column - use with care. |
| */ |
| private void immediateUpdate() { |
| fParentRuler.immediateUpdate(); |
| } |
| |
| /** |
| * Returns the width of the column. |
| * |
| * @return the width of the column |
| */ |
| private int getWidth() { |
| return fColumn.getWidth(); |
| } |
| |
| /** |
| * Returns the System background color for list widgets. |
| * |
| * @return the System background color for list widgets |
| */ |
| private Color getBackground() { |
| if (fBackground == null) |
| return fWidget.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND); |
| return fBackground; |
| } |
| |
| /** |
| * Sets the hover later returned by {@link #getHover()}. |
| * |
| * @param hover the hover |
| */ |
| public void setHover(IAnnotationHover hover) { |
| // TODO ignore for now - must make revision hover settable from outside |
| } |
| |
| /** |
| * Returns <code>true</code> if the receiver can provide a hover for a certain document line. |
| * |
| * @param activeLine the document line of interest |
| * @return <code>true</code> if the receiver can provide a hover |
| */ |
| public boolean hasHover(int activeLine) { |
| return fViewer instanceof ISourceViewer && fHover.getHoverLineRange((ISourceViewer) fViewer, activeLine) != null; |
| } |
| |
| /** |
| * Returns the revision at a certain document offset, or <code>null</code> for none. |
| * |
| * @param offset the document offset |
| * @return the revision at offset, or <code>null</code> for none |
| */ |
| Revision getRevision(int offset) { |
| IDocument document= fViewer.getDocument(); |
| int line; |
| try { |
| line= document.getLineOfOffset(offset); |
| } catch (BadLocationException x) { |
| return null; |
| } |
| if (line != -1) { |
| RevisionRange range= getRange(line); |
| if (range != null) |
| return range.getRevision(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns <code>true</code> if a revision model has been set, <code>false</code> otherwise. |
| * |
| * @return <code>true</code> if a revision model has been set, <code>false</code> otherwise |
| */ |
| public boolean hasInformation() { |
| return fRevisionInfo != null; |
| } |
| |
| /** |
| * Returns the width in chars required to display information. |
| * |
| * @return the width in chars required to display information |
| * @since 3.3 |
| */ |
| public int getRequiredWidth() { |
| if (fRequiredWidth == -1) { |
| if (hasInformation() && (fShowRevision || fShowAuthor)) { |
| int revisionWidth= 0; |
| int authorWidth= 0; |
| for (Revision revision : fRevisionInfo.getRevisions()) { |
| revisionWidth= Math.max(revisionWidth, revision.getId().length()); |
| authorWidth= Math.max(authorWidth, revision.getAuthor().length()); |
| } |
| fRevisionIdChars= revisionWidth + 1; |
| if (fShowAuthor && fShowRevision) |
| fRequiredWidth= revisionWidth + authorWidth + 2; |
| else if (fShowAuthor) |
| fRequiredWidth= authorWidth + 1; |
| else |
| fRequiredWidth= revisionWidth + 1; |
| } else { |
| fRequiredWidth= 0; |
| } |
| } |
| return fRequiredWidth; |
| } |
| |
| /** |
| * Enables showing the revision id. |
| * |
| * @param show <code>true</code> to show the revision, <code>false</code> to hide it |
| */ |
| public void showRevisionId(boolean show) { |
| if (fShowRevision != show) { |
| fRequiredWidth= -1; |
| fRevisionIdChars= 0; |
| fShowRevision= show; |
| postRedraw(); |
| } |
| } |
| |
| /** |
| * Enables showing the revision author. |
| * |
| * @param show <code>true</code> to show the author, <code>false</code> to hide it |
| */ |
| public void showRevisionAuthor(boolean show) { |
| if (fShowAuthor != show) { |
| fRequiredWidth= -1; |
| fRevisionIdChars= 0; |
| fShowAuthor= show; |
| postRedraw(); |
| } |
| } |
| |
| /** |
| * Adds a revision listener. |
| * |
| * @param listener the listener |
| * @since 3.3 |
| */ |
| public void addRevisionListener(IRevisionListener listener) { |
| fRevisionListeners.add(listener); |
| } |
| |
| /** |
| * Removes a revision listener. |
| * |
| * @param listener the listener |
| * @since 3.3 |
| */ |
| public void removeRevisionListener(IRevisionListener listener) { |
| fRevisionListeners.remove(listener); |
| } |
| |
| /** |
| * Informs the revision listeners about a change. |
| * |
| * @since 3.3 |
| */ |
| private void informListeners() { |
| if (fRevisionInfo == null || fRevisionListeners.isEmpty()) |
| return; |
| |
| RevisionEvent event= new RevisionEvent(fRevisionInfo); |
| for (IRevisionListener listener : fRevisionListeners) { |
| listener.revisionInformationChanged(event); |
| } |
| } |
| } |