blob: 2c1d4ccc80838411377c227dc8dfaf5aab8dbd7b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.text.source;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IAnnotationHover;
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.ILineDiffInfo;
import org.eclipse.jface.text.source.ILineDiffer;
import org.eclipse.jface.text.source.ILineDifferExtension2;
import org.eclipse.jface.text.source.ILineRange;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.IVerticalRulerColumn;
/**
* A strategy for painting the quick diff colors onto the vertical ruler column. It also manages the
* quick diff hover.
*
* @since 3.2
*/
public final class DiffPainter {
/**
* Internal listener class that will update the ruler when the underlying model changes.
*/
private class AnnotationListener implements IAnnotationModelListener {
@Override
public void modelChanged(IAnnotationModel model) {
postRedraw();
}
}
/** 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 line differ extracted from the annotation model. */
private ILineDiffer fLineDiffer= null;
/** Color for changed lines */
private Color fAddedColor;
/** Color for added lines */
private Color fChangedColor;
/** Color for the deleted line indicator */
private Color fDeletedColor;
/** The background color. */
private Color fBackground;
/** The ruler's hover */
private IAnnotationHover fHover;
/** The internal listener */
private final AnnotationListener fAnnotationListener= new AnnotationListener();
/** The shared color provider, possibly <code>null</code>. */
private final ISharedTextColors fSharedColors;
/**
* The zoom level for the current painting operation. Workaround for bug 516293.
* @since 3.12
*/
private int fZoom= 100;
/**
* Creates a new diff 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, may be
* <code>null</code>
*/
public DiffPainter(IVerticalRulerColumn column, ISharedTextColors sharedColors) {
Assert.isLegal(column != null);
fColumn= column;
fSharedColors= sharedColors;
}
/**
* 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 quick diff hover later returned by {@link #getHover()}.
*
* @param hover the hover
*/
public void setHover(IAnnotationHover hover) {
fHover= hover;
}
/**
* Returns the quick diff hover set by {@link #setHover(IAnnotationHover)}.
*
* @return the quick diff hover set by {@link #setHover(IAnnotationHover)}
*/
public IAnnotationHover getHover() {
return fHover;
}
/**
* Sets the background color.
*
* @param background the background color, <code>null</code> to use the platform's list background
*/
public void setBackground(Color background) {
fBackground= background;
}
/**
* 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 visibleModelLines the lines (in document offsets) that are currently (perhaps only
* partially) visible
*/
public void paint(GC gc, ILineRange visibleModelLines) {
connectIfNeeded();
if (!isConnected())
return;
// draw diff info
final int lastLine= end(visibleModelLines);
final int width= getWidth();
final Color deletionColor= getDeletionColor();
for (int line= visibleModelLines.getStartLine(); line < lastLine; line++) {
paintLine(line, gc, width, deletionColor);
}
}
/**
* 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.addDisposeListener(e -> handleDispose());
}
/**
* 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;
}
/**
* Disposes of this painter and releases any resources.
*/
private void handleDispose() {
if (fLineDiffer != null) {
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= null;
}
}
/**
* Paints a single model line onto <code>gc</code>.
*
* @param line the model line to paint
* @param gc the {@link GC} to paint onto
* @param width the width of the column
* @param deletionColor the background color used to indicate deletions
*/
private void paintLine(int line, GC gc, int width, Color deletionColor) {
int widgetLine= JFaceTextUtil.modelLineToWidgetLine(fViewer, line);
if (widgetLine == -1)
return;
ILineDiffInfo info= getDiffInfo(line);
if (info != null) {
int y= fWidget.getLinePixel(widgetLine);
int lineHeight = JFaceTextUtil.computeLineHeight(fWidget, widgetLine);
// draw background color if special
if (hasSpecialColor(info)) {
gc.setBackground(getColor(info));
gc.fillRectangle(0, autoScaleUp(y), autoScaleUp(width), autoScaleUp(lineHeight));
}
/* Deletion Indicator: Simply a horizontal line */
int delBefore= info.getRemovedLinesAbove();
int delBelow= info.getRemovedLinesBelow();
if (delBefore > 0 || delBelow > 0) {
gc.setForeground(deletionColor);
gc.setLineWidth(autoScaleUp(1));
if (delBefore > 0)
gc.drawLine(0, autoScaleUp(y), autoScaleUp(width), autoScaleUp(y));
if (delBelow > 0)
gc.drawLine(0, autoScaleUp(y + lineHeight - 1), autoScaleUp(width), autoScaleUp(y + lineHeight - 1));
}
}
}
/**
* Returns whether the line background differs from the default.
*
* @param info the info being queried
* @return <code>true</code> if <code>info</code> describes either a changed or an added
* line.
*/
private boolean hasSpecialColor(ILineDiffInfo info) {
return info.getChangeType() == ILineDiffInfo.ADDED || info.getChangeType() == ILineDiffInfo.CHANGED;
}
/**
* Retrieves the <code>ILineDiffInfo</code> for <code>line</code> from the model. There are
* optimizations for direct access and sequential access patterns.
*
* @param line the line we want the info for.
* @return the <code>ILineDiffInfo</code> for <code>line</code>, or <code>null</code>.
*/
private ILineDiffInfo getDiffInfo(int line) {
if (fLineDiffer != null)
return fLineDiffer.getLineInfo(line);
return null;
}
/**
* Returns the color for deleted lines.
*
* @return the color to be used for the deletion indicator
*/
private Color getDeletionColor() {
return fDeletedColor == null ? getBackground() : fDeletedColor;
}
/**
* Returns the color for the given line diff info.
*
* @param info the <code>ILineDiffInfo</code> being queried
* @return the correct background color for the line type being described by <code>info</code>
*/
private Color getColor(ILineDiffInfo info) {
Assert.isTrue(info != null && info.getChangeType() != ILineDiffInfo.UNCHANGED);
Color ret= null;
switch (info.getChangeType()) {
case ILineDiffInfo.CHANGED:
ret= getShadedColor(fChangedColor);
break;
case ILineDiffInfo.ADDED:
ret= getShadedColor(fAddedColor);
break;
}
return ret == null ? getBackground() : ret;
}
/**
* Sets the background color for changed lines.
*
* @param color the new color to be used for the changed lines background
* @return the shaded color
*/
private Color getShadedColor(Color color) {
if (color == null)
return null;
if (fSharedColors == null)
return color;
RGB baseRGB= color.getRGB();
RGB background= getBackground().getRGB();
boolean darkBase= isDark(baseRGB);
boolean darkBackground= isDark(background);
if (darkBase && darkBackground)
background= new RGB(255, 255, 255);
else if (!darkBase && !darkBackground)
background= new RGB(0, 0, 0);
return fSharedColors.getColor(interpolate(baseRGB, background, 0.6));
}
/**
* Sets the annotation model.
*
* @param model the annotation model, possibly <code>null</code>
* @see IVerticalRulerColumn#setModel(IAnnotationModel)
*/
public void setModel(IAnnotationModel model) {
IAnnotationModel newModel;
if (model instanceof IAnnotationModelExtension)
newModel= ((IAnnotationModelExtension) model).getAnnotationModel(IChangeRulerColumn.QUICK_DIFF_MODEL_ID);
else
newModel= model;
setDiffer(newModel);
}
/**
* Sets the line differ.
*
* @param differ the line differ
*/
private void setDiffer(IAnnotationModel differ) {
if (differ instanceof ILineDiffer) {
if (fLineDiffer != differ) {
if (fLineDiffer != null)
((IAnnotationModel) fLineDiffer).removeAnnotationModelListener(fAnnotationListener);
fLineDiffer= (ILineDiffer) differ;
((IAnnotationModel) fLineDiffer).addAnnotationModelListener(fAnnotationListener);
}
}
}
/**
* 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());
}
}
}
/**
* Triggers redrawing of the column.
*/
private void redraw() {
fColumn.redraw();
}
/**
* Returns the width of the column.
*
* @return the width of the column
*/
private int getWidth() {
return fColumn.getWidth();
}
/**
* 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 System background color for list widgets or the set background.
*
* @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 color for added lines.
*
* @param addedColor the color for added lines
* @see org.eclipse.jface.text.source.IChangeRulerColumn#setAddedColor(org.eclipse.swt.graphics.Color)
*/
public void setAddedColor(Color addedColor) {
fAddedColor= addedColor;
}
/**
* Sets the color for changed lines.
*
* @param changedColor the color for changed lines
* @see org.eclipse.jface.text.source.IChangeRulerColumn#setChangedColor(org.eclipse.swt.graphics.Color)
*/
public void setChangedColor(Color changedColor) {
fChangedColor= changedColor;
}
/**
* Sets the color for deleted lines.
*
* @param deletedColor the color for deleted lines
* @see org.eclipse.jface.text.source.IChangeRulerColumn#setDeletedColor(org.eclipse.swt.graphics.Color)
*/
public void setDeletedColor(Color deletedColor) {
fDeletedColor= deletedColor;
}
/**
* 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 true;
}
/**
* Returns the display character for the accessibility mode for a certain model line.
*
* @param line the document line of interest
* @return the display character for <code>line</code>
*/
public String getDisplayCharacter(int line) {
return getDisplayCharacter(getDiffInfo(line));
}
/**
* Returns the character to display in character display mode for the given
* <code>ILineDiffInfo</code>
*
* @param info the <code>ILineDiffInfo</code> being queried
* @return the character indication for <code>info</code>
*/
private String getDisplayCharacter(ILineDiffInfo info) {
if (info == null)
return " "; //$NON-NLS-1$
switch (info.getChangeType()) {
case ILineDiffInfo.CHANGED:
return "~"; //$NON-NLS-1$
case ILineDiffInfo.ADDED:
return "+"; //$NON-NLS-1$
}
return " "; //$NON-NLS-1$
}
/**
* Returns a specification of a color that lies between the given foreground and background
* color using the given scale factor.
*
* @param fg the foreground color
* @param bg the background color
* @param scale the scale factor
* @return the interpolated color
*/
private static RGB interpolate(RGB fg, RGB bg, double scale) {
return new RGB((int) ((1.0 - scale) * fg.red + scale * bg.red), (int) ((1.0 - scale) * fg.green + scale * bg.green), (int) ((1.0 - scale) * fg.blue + scale * bg.blue));
}
/**
* Returns the grey value in which the given color would be drawn in grey-scale.
*
* @param rgb the color
* @return the grey-scale value
*/
private static double greyLevel(RGB rgb) {
if (rgb.red == rgb.green && rgb.green == rgb.blue)
return rgb.red;
return (0.299 * rgb.red + 0.587 * rgb.green + 0.114 * rgb.blue + 0.5);
}
/**
* Returns whether the given color is dark or light depending on the colors grey-scale level.
*
* @param rgb the color
* @return <code>true</code> if the color is dark, <code>false</code> if it is light
*/
private static boolean isDark(RGB rgb) {
return greyLevel(rgb) > 128;
}
/**
* Returns <code>true</code> if diff information is being displayed, <code>false</code> otherwise.
*
* @return <code>true</code> if diff information is being displayed, <code>false</code> otherwise
* @since 3.3
*/
public boolean hasInformation() {
if (fLineDiffer instanceof ILineDifferExtension2)
return !((ILineDifferExtension2) fLineDiffer).isSuspended();
return fLineDiffer != null;
}
}