| /******************************************************************************* |
| * 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; |
| } |
| } |