blob: 7c0bd445c1f5a834a6e9fd8f982e57d6c8337445 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2008 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.text.source;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.IViewportListener;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.TextEvent;
/**
* Abstract implementation of a {@link IVerticalRulerColumn} that
* uses a {@link Canvas} to draw the ruler contents and which
* handles scrolling and mouse selection.
*
* <h3>Painting</h3>
* Subclasses can hook into the paint loop at three levels:
* <ul>
* <li>Override <strong>{@link #paint(GC, ILineRange)}</strong> to control the entire painting of
* the ruler.</li>
* <li>Override <strong>{@link #paintLine(GC, int, int, int, int)}</strong> to control the
* painting of a line.</li>
* <li>Leave the painting to the default implementation, but override <strong>{@link #computeBackground(int)}</strong>,
* <strong>{@link #computeForeground(int)}</strong> and <strong>{@link #computeText(int)}</strong>
* to specify the ruler appearance for a line.</li>
* </ul>
*
* <h3>Invalidation</h3>
* Subclasses may call {@link #redraw()} to mark the entire ruler as needing to be redrawn.
* Alternatively, use {@link #redraw(ILineRange)} to only invalidate a certain line range, for
* example due to changes to the display model.
*
* <h3>Configuration</h3>
* Subclasses can set the following properties. Setting them may trigger redrawing.
* <ul>
* <li>The {@link #setFont(Font) font} used to draw text in {@link #paintLine(GC, int, int, int, int)}.</li>
* <li>The horizontal {@link #setTextInset(int) text inset} for text drawn.</li>
* <li>The {@link #setDefaultBackground(Color) default background color} of the ruler.</li>
* <li>The {@link #setWidth(int) width} of the ruler.</li>
* </ul>
*
* @since 3.3
*/
public abstract class AbstractRulerColumn implements IVerticalRulerColumn, IVerticalRulerInfo, IVerticalRulerInfoExtension {
private static final int DEFAULT_WIDTH= 12;
private static final int DEFAULT_TEXT_INSET= 2;
/**
* Handles all the mouse interaction in this line number ruler column.
*/
private final class MouseHandler implements MouseListener, MouseMoveListener {
@Override
public void mouseUp(MouseEvent event) {
}
@Override
public void mouseDown(MouseEvent event) {
fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
}
@Override
public void mouseDoubleClick(MouseEvent event) {
fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
}
@Override
public void mouseMove(MouseEvent event) {
fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
}
}
/**
* Internal listener class that updates the ruler upon scrolling and text modifications.
*/
private final class InternalListener implements IViewportListener, ITextListener {
@Override
public void viewportChanged(int topPixel) {
int delta= topPixel - fLastTopPixel;
if (scrollVertical(delta))
fCanvas.update(); // force update the invalidated regions
}
@Override
public void textChanged(TextEvent event) {
/*
* Redraw: - when the viewer is drawing, and any of the following - the widget was not
* full before the change - the widget is not full after the change - the document event
* was a visual modification (no document event attached) - for example when the
* projection changes.
*/
if (!event.getViewerRedrawState())
return;
if (fWasShowingEntireContents || event.getDocumentEvent() == null || JFaceTextUtil.isShowingEntireContents(fStyledText))
redraw();
}
}
/* Listeners */
/** The viewport listener. */
private final InternalListener fInternalListener= new InternalListener();
/** The mouse handler. */
private final MouseHandler fMouseHandler= new MouseHandler();
/*
* Implementation and context of this ruler - created and set in createControl(), disposed of in
* columnRemoved().
*/
/** The parent ruler, possibly <code>null</code>. */
private CompositeRuler fParentRuler;
/** The canvas, the only widget used to draw this ruler, possibly <code>null</code>. */
private Canvas fCanvas;
/** The text viewer, possibly <code>null</code>. */
private ITextViewer fTextViewer;
/** The text viewer's widget, possibly <code>null</code>. */
private StyledText fStyledText;
/* State when the canvas was last painted. */
/** The text widget's top pixel when the ruler was last painted. */
private int fLastTopPixel= -1;
/** Whether the text widget was showing the entire contents when the ruler was last painted. */
private boolean fWasShowingEntireContents= false;
/* Configuration */
/** The width of this ruler. */
private int fWidth= DEFAULT_WIDTH;
/** The text inset. */
private int fTextInset= DEFAULT_TEXT_INSET;
/** The default background color, <code>null</code> to use the text viewer's background color. */
private Color fBackground;
/** The font, <code>null</code> to use the default font. */
private Font fFont;
/** The annotation model, possibly <code>null</code>. */
private IAnnotationModel fModel;
/** The annotation hover, possibly <code>null</code>. */
private IAnnotationHover fHover;
/**
* Creates a new ruler.
*/
protected AbstractRulerColumn() {
}
@Override
public Control createControl(CompositeRuler parentRuler, Composite parentControl) {
Assert.isLegal(parentControl != null);
Assert.isLegal(parentRuler != null);
Assert.isLegal(fParentRuler == null); // only call when not yet initialized!
fParentRuler= parentRuler;
fTextViewer= getParentRuler().getTextViewer();
fTextViewer.addViewportListener(fInternalListener);
fTextViewer.addTextListener(fInternalListener);
fStyledText= fTextViewer.getTextWidget();
fCanvas= new Canvas(parentControl, getCanvasStyle());
fCanvas.setBackground(getDefaultBackground());
fCanvas.setFont(getFont());
fCanvas.addPaintListener(event -> AbstractRulerColumn.this.paintControl(event));
fCanvas.addMouseListener(fMouseHandler);
fCanvas.addMouseMoveListener(fMouseHandler);
return fCanvas;
}
/**
* Returns the SWT style bits used when creating the ruler canvas.
* <p>
* The default implementation returns <code>SWT.NO_BACKGROUND</code>.</p>
* <p>
* Clients may reimplement this method to create a canvas with their
* desired style bits.</p>
*
* @return the SWT style bits, or <code>SWT.NONE</code> if none
*/
protected int getCanvasStyle() {
return SWT.NO_BACKGROUND;
}
@Override
public final Control getControl() {
return fCanvas;
}
/**
* The new width in pixels. The <code>DEFAULT_WIDTH</code> constant
* specifies the default width.
*
* @param width the new width
*/
protected final void setWidth(int width) {
Assert.isLegal(width >= 0);
if (fWidth != width) {
fWidth= width;
CompositeRuler composite= getParentRuler();
if (composite != null)
composite.relayout();
}
}
@Override
public final int getWidth() {
return fWidth;
}
/**
* Returns the parent ruler, <code>null</code> before
* {@link #createControl(CompositeRuler, Composite)} has been called.
*
* @return the parent ruler or <code>null</code>
*/
protected final CompositeRuler getParentRuler() {
return fParentRuler;
}
/**
* {@inheritDoc}
*
* @param font the font or <code>null</code> to use the default font
*/
@Override
public final void setFont(Font font) {
if (fFont != font) {
fFont= font;
redraw();
}
}
/**
* Returns the current font. If a font has not been explicitly set, the widget's font is
* returned.
*
* @return the font used to render text on the ruler.
*/
protected final Font getFont() {
if (fFont != null)
return fFont;
if (fStyledText != null && !fStyledText.isDisposed())
return fStyledText.getFont();
return JFaceResources.getTextFont();
}
/**
* Sets the text inset (padding) used to draw text in {@link #paintLine(GC, int, int, int, int)}.
*
* @param textInset the new text inset
*/
protected final void setTextInset(int textInset) {
if (textInset != fTextInset) {
fTextInset= textInset;
redraw();
}
}
/**
* Returns the text inset for text drawn by {@link #paintLine(GC, int, int, int, int)}. The
* <code>DEFAULT_TEXT_INSET</code> constant specifies the default inset in pixels.
*
* @return the text inset for text
*/
protected final int getTextInset() {
return fTextInset;
}
@Override
public void setModel(IAnnotationModel model) {
if (fModel != model) {
fModel= model;
redraw();
}
}
@Override
public final IAnnotationModel getModel() {
return fModel;
}
/**
* Sets the default background color for this column. The default background is used as default
* implementation of {@link #computeBackground(int)} and also to paint the area of the ruler
* that does not correspond to any lines (when the viewport is not entirely filled with lines).
*
* @param background the default background color, <code>null</code> to use the text widget's
* background
*/
protected final void setDefaultBackground(Color background) {
if (fBackground != background) {
fBackground= background;
if (fCanvas != null && !fCanvas.isDisposed())
fCanvas.setBackground(getDefaultBackground());
redraw();
}
}
/**
* Returns the background color. May return <code>null</code> if the system is shutting down.
*
* @return the background color
*/
protected final Color getDefaultBackground() {
if (fBackground != null)
return fBackground;
if (fStyledText != null && !fStyledText.isDisposed())
return fStyledText.getBackground();
Display display;
if (fCanvas != null && !fCanvas.isDisposed())
display= fCanvas.getDisplay();
else
display= Display.getCurrent();
if (display != null)
return display.getSystemColor(SWT.COLOR_LIST_BACKGROUND);
return null;
}
/**
* Sets the annotation hover.
*
* @param hover the annotation hover, <code>null</code> for no hover
*/
protected final void setHover(IAnnotationHover hover) {
if (fHover != hover)
fHover= hover;
}
@Override
public IAnnotationHover getHover() {
return fHover;
}
/**
* Disposes this ruler column.
* <p>
* Subclasses may extend this method.</p>
* <p>
* Clients who created this column are responsible to call this method
* once the column is no longer used.</p>
*/
public void dispose() {
if (fTextViewer != null) {
fTextViewer.removeViewportListener(fInternalListener);
fTextViewer.removeTextListener(fInternalListener);
fTextViewer= null;
}
if (fStyledText != null)
fStyledText= null;
if (fCanvas != null) {
fCanvas.dispose();
fCanvas= null;
}
}
@Override
public final void redraw() {
if (fCanvas != null && !fCanvas.isDisposed())
fCanvas.redraw();
}
/**
* Marks the region covered by <code>lines</code> as needing to be redrawn.
*
* @param lines the lines to be redrawn in document coordinates
*/
protected final void redraw(ILineRange lines) {
if (fCanvas == null || fCanvas.isDisposed())
return;
int firstModelLine= lines.getStartLine();
int lastModelLine= firstModelLine + lines.getNumberOfLines();
int firstWidgetLine= JFaceTextUtil.modelLineToWidgetLine(fTextViewer, firstModelLine);
int lastWidgetLine= JFaceTextUtil.modelLineToWidgetLine(fTextViewer, lastModelLine);
int from= Math.max(0, fStyledText.getLinePixel(firstWidgetLine));
// getLinePixel will return the last pixel of the last line if line == lineCount
int to= Math.min(fCanvas.getSize().y, fStyledText.getLinePixel(lastWidgetLine + 1));
fCanvas.redraw(0, from, fWidth, to - from, false);
}
/**
* Paints the ruler column.
*
* @param event the paint event
*/
private void paintControl(PaintEvent event) {
if (fTextViewer == null)
return;
fWasShowingEntireContents= JFaceTextUtil.isShowingEntireContents(fStyledText);
fLastTopPixel= fStyledText.getTopPixel();
ILineRange lines= computeDirtyWidgetLines(event);
GC gc= event.gc;
paint(gc, lines);
if ((fCanvas.getStyle() & SWT.NO_BACKGROUND) != 0) {
// fill empty area below any lines
int firstEmpty= Math.max(event.y, fStyledText.getLinePixel(fStyledText.getLineCount()));
int lastEmpty= event.y + event.height;
if (lastEmpty > firstEmpty) {
gc.setBackground(getDefaultBackground());
gc.fillRectangle(0, firstEmpty, getWidth(), lastEmpty - firstEmpty);
}
}
}
/**
* Computes the widget lines that need repainting given the clipping region of a paint event.
*
* @param event the paint event
* @return the lines in widget coordinates that need repainting
*/
private ILineRange computeDirtyWidgetLines(PaintEvent event) {
int firstLine= fStyledText.getLineIndex(event.y);
int lastLine= fStyledText.getLineIndex(event.y + event.height - 1);
return new LineRange(firstLine, lastLine - firstLine + 1);
}
/**
* Paints the ruler. Note that <code>lines</code> reference widget line indices, and that
* <code>lines</code> may not cover the entire viewport, but only the lines that need to be
* painted. The lines may not be entirely visible.
* <p>
* Subclasses may replace or extend. The default implementation calls
* {@link #paintLine(GC, int, int, int, int)} for every visible line.
* </p>
*
* @param gc the graphics context to paint on
* @param lines the lines to paint in widget coordinates
*/
protected void paint(GC gc, ILineRange lines) {
final int firstLine= lines.getStartLine();
final int lastLine= firstLine + lines.getNumberOfLines();
for (int line= firstLine; line < lastLine; line++) {
int modelLine= JFaceTextUtil.widgetLine2ModelLine(fTextViewer, line);
if (modelLine == -1)
continue;
int linePixel= fStyledText.getLinePixel(line);
int lineHeight= fStyledText.getLineHeight(fStyledText.getOffsetAtLine(line));
paintLine(gc, modelLine, line, linePixel, lineHeight);
}
}
/**
* Paints the ruler representation of a single line.
* <p>
* Subclasses may replace or extend. The default implementation draws the text obtained by
* {@link #computeText(int)} in the {@link #computeForeground(int) foreground color} and fills
* the entire width using the {@link #computeBackground(int) background color}. The text is
* drawn {@link #getTextInset()} pixels to the right of the left border.
* </p>
*
* @param gc the graphics context to paint on
* @param modelLine the model line (based on document coordinates)
* @param widgetLine the line in the text widget corresponding to <code>modelLine</code>
* @param linePixel the first y-pixel of the widget line
* @param lineHeight the line height in pixels
*/
protected void paintLine(GC gc, int modelLine, int widgetLine, int linePixel, int lineHeight) {
gc.setBackground(computeBackground(modelLine));
gc.fillRectangle(0, linePixel, getWidth(), lineHeight);
String text= computeText(modelLine);
if (text != null) {
gc.setForeground(computeForeground(modelLine));
gc.drawString(text, getTextInset(), linePixel, true);
}
}
/**
* Returns the text to be drawn for a certain line by {@link #paintLine(GC, int, int, int, int)},
* <code>null</code> for no text. The default implementation returns <code>null</code>.
* <p>
* Subclasses may replace or extend.
* </p>
*
* @param line the document line number
* @return the text to be drawn for the given line, <code>null</code> for no text
*/
protected String computeText(int line) {
return null;
}
/**
* Returns the background color drawn for a certain line by
* {@link #paintLine(GC, int, int, int, int)}. The default implementation returns
* {@link #getDefaultBackground()}.
* <p>
* Subclasses may replace or extend.
* </p>
*
* @param line the document line number
* @return the background color for drawn for the given line
*/
protected Color computeBackground(int line) {
return getDefaultBackground();
}
/**
* Returns the foreground color drawn for a certain line by
* {@link #paintLine(GC, int, int, int, int)}. The default implementation returns a
* {@link SWT#COLOR_DARK_GRAY} color.
* <p>
* Subclasses may replace or extend.
* </p>
*
* @param line the document line number
* @return the foreground color for drawn for the given line
*/
protected Color computeForeground(int line) {
return fStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
}
@Override
public final int getLineOfLastMouseButtonActivity() {
return getParentRuler().getLineOfLastMouseButtonActivity();
}
@Override
public final int toDocumentLineNumber(int y_coordinate) {
return getParentRuler().toDocumentLineNumber(y_coordinate);
}
@Override
public void addVerticalRulerListener(IVerticalRulerListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeVerticalRulerListener(IVerticalRulerListener listener) {
throw new UnsupportedOperationException();
}
/**
* Scrolls the canvas vertically (adapted from
* {@linkplain StyledText StyledText.scrollVertical()}).
*
* @param pixels the number of pixels to scroll (negative to scroll upwards)
* @return <code>true</code> if the widget was scrolled, <code>false</code> if the widget
* was not scrolled
*/
private boolean scrollVertical(int pixels) {
if (pixels == 0 || fCanvas == null || fCanvas.isDisposed())
return false;
final int width= getWidth();
final int clientAreaHeight= fStyledText.getClientArea().height;
final int topMargin= 0;
final int leftMargin= 0;
final int bottomMargin= 0;
if (pixels > 0) {
// downwards scrolling - content moves upwards
int sourceY= topMargin + pixels;
int scrollHeight= clientAreaHeight - sourceY - bottomMargin;
if (scrollHeight > 0)
// scroll recycled area
fCanvas.scroll(leftMargin, topMargin, leftMargin, sourceY, width, scrollHeight, true);
if (sourceY > scrollHeight) {
// redraw in-between area
int redrawY= Math.max(0, topMargin + scrollHeight);
int redrawHeight= Math.min(clientAreaHeight, pixels - scrollHeight);
fCanvas.redraw(leftMargin, redrawY, width, redrawHeight, true);
}
} else {
// upwards scrolling - content moves downwards
int destinationY= topMargin - pixels;
int scrollHeight= clientAreaHeight - destinationY - bottomMargin;
if (scrollHeight > 0)
// scroll recycled area
fCanvas.scroll(leftMargin, destinationY, leftMargin, topMargin, width, scrollHeight, true);
if (destinationY > scrollHeight) {
// redraw in-between area
int redrawY= Math.max(0, topMargin + scrollHeight);
int redrawHeight= Math.min(clientAreaHeight, -pixels - scrollHeight);
fCanvas.redraw(leftMargin, redrawY, width, redrawHeight, true);
}
}
return true;
}
}