blob: db95c99afe159dd17635bf483c936c0e998e5781 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2010 Wind River Systems, Inc., IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Anton Leherbauer (Wind River Systems) - initial API and implementation - https://bugs.eclipse.org/bugs/show_bug.cgi?id=22712
* Anton Leherbauer (Wind River Systems) - [painting] Long lines take too long to display when "Show Whitespace Characters" is enabled - https://bugs.eclipse.org/bugs/show_bug.cgi?id=196116
* Anton Leherbauer (Wind River Systems) - [painting] Whitespace characters not drawn when scrolling to right slowly - https://bugs.eclipse.org/bugs/show_bug.cgi?id=206633
* Tom Eicher (Avaloq Evolution AG) - block selection mode
* Stephane Begaudeau (Obeo) - Acceleo whitespaces shouldn't be drawn out of String literals
*******************************************************************************/
package org.eclipse.acceleo.internal.ide.ui.editors.template;
import org.eclipse.acceleo.ide.ui.AcceleoUIActivator;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPaintPositionManager;
import org.eclipse.jface.text.IPainter;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.ocl.ecore.StringLiteralExp;
import org.eclipse.ocl.utilities.ASTNode;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.StyledTextContent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
/**
* This painter will draw visible characters for the invisible characters in the Acceleo Editor. Created from
* org.eclipse.jface.text.WhitespaceCharacterPainter.
*
* @author <a href="mailto:stephane.begaudeau@obeo.fr">Stephane Begaudeau</a>
*/
public class AcceleoWhitespaceCharactersPainter implements IPainter, PaintListener {
/**
* The space sign.
*/
private static final char SPACE_SIGN = '\u00b7';
/**
* The ideographic space sign.
*/
private static final char IDEOGRAPHIC_SPACE_SIGN = '\u00b0';
/**
* The tab sign.
*/
private static final char TAB_SIGN = '\u00bb';
/**
* The carriage return sign.
*/
private static final char CARRIAGE_RETURN_SIGN = '\u00a4';
/**
* The line feed sign.
*/
private static final char LINE_FEED_SIGN = '\u00b6';
/**
* Indicates whether this painter is active.
**/
private boolean fIsActive;
/**
* The Acceleo Editor.
*/
private AcceleoEditor fEditor;
/**
* The source viewer this painter is attached to.
**/
private ITextViewer fTextViewer;
/**
* The viewer's widget.
**/
private StyledText fTextWidget;
/**
* Tells whether the advanced graphics sub system is available.
**/
private final boolean fIsAdvancedGraphicsPresent;
/**
* The constructor.
*
* @param editor
* The acceleo editor
*/
public AcceleoWhitespaceCharactersPainter(AcceleoEditor editor) {
super();
this.fEditor = editor;
this.fTextViewer = editor.getAcceleoSourceViewer();
this.fTextWidget = this.fTextViewer.getTextWidget();
GC gc = new GC(this.fTextWidget);
gc.setAdvanced(true);
this.fIsAdvancedGraphicsPresent = gc.getAdvanced();
gc.dispose();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent)
*/
public void paintControl(PaintEvent event) {
if (this.fTextWidget != null) {
handleDrawRequest(event.gc, event.x, event.y, event.width, event.height);
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.IPainter#dispose()
*/
public void dispose() {
fTextViewer = null;
fTextWidget = null;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.IPainter#paint(int)
*/
public void paint(int reason) {
IDocument document = this.fTextViewer.getDocument();
if (document == null) {
deactivate(false);
return;
}
if (!this.fIsActive) {
this.fIsActive = true;
this.fTextWidget.addPaintListener(this);
redrawAll();
} else if (reason == CONFIGURATION || reason == INTERNAL) {
redrawAll();
} else if (reason == TEXT_CHANGE) {
// redraw current line only
try {
IRegion lineRegion = document.getLineInformationOfOffset(getDocumentOffset(this.fTextWidget
.getCaretOffset()));
int widgetOffset = getWidgetOffset(lineRegion.getOffset());
int charCount = this.fTextWidget.getCharCount();
int redrawLength = Math.min(lineRegion.getLength(), charCount - widgetOffset);
if (widgetOffset >= 0 && redrawLength > 0) {
this.fTextWidget.redrawRange(widgetOffset, redrawLength, true);
}
} catch (BadLocationException e) {
// ignore
AcceleoUIActivator.log(e, true);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.IPainter#deactivate(boolean)
*/
public void deactivate(boolean redraw) {
if (this.fIsActive) {
this.fIsActive = false;
this.fTextWidget.removePaintListener(this);
if (redraw) {
redrawAll();
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.IPainter#setPositionManager(org.eclipse.jface.text.IPaintPositionManager)
*/
public void setPositionManager(IPaintPositionManager manager) {
// no need for a position manager
}
/**
* Draw characters in view range.
*
* @param gc
* The GC
* @param x
* x
* @param y
* y
* @param w
* The width
* @param h
* The height
*/
private void handleDrawRequest(GC gc, int x, int y, int w, int h) {
int startLine = this.fTextWidget.getLineIndex(y);
int endLine = this.fTextWidget.getLineIndex(y + h - 1);
if (startLine <= endLine && startLine < this.fTextWidget.getLineCount()) {
if (this.fIsAdvancedGraphicsPresent) {
int alpha = gc.getAlpha();
gc.setAlpha(100);
drawLineRange(gc, startLine, endLine, x, w);
gc.setAlpha(alpha);
} else {
drawLineRange(gc, startLine, endLine, x, w);
}
}
}
/**
* Draw the given line range.
*
* @param gc
* the GC
* @param startLine
* first line number
* @param endLine
* last line number (inclusive)
* @param x
* the X-coordinate of the drawing range
* @param w
* the width of the drawing range
*/
private void drawLineRange(GC gc, int startLine, int endLine, int x, int w) {
final int viewPortWidth = fTextWidget.getClientArea().width;
for (int line = startLine; line <= endLine; line++) {
int lineOffset = fTextWidget.getOffsetAtLine(line);
// line end offset including line delimiter
int lineEndOffset;
if (line < fTextWidget.getLineCount() - 1) {
lineEndOffset = fTextWidget.getOffsetAtLine(line + 1);
} else {
lineEndOffset = fTextWidget.getCharCount();
}
// line length excluding line delimiter
int lineLength = lineEndOffset - lineOffset;
while (lineLength > 0) {
char c = fTextWidget.getTextRange(lineOffset + lineLength - 1, 1).charAt(0);
if (c != '\r' && c != '\n') {
break;
}
--lineLength;
}
// compute coordinates of last character on line
Point endOfLine = fTextWidget.getLocationAtOffset(lineOffset + lineLength);
if (x - endOfLine.x > viewPortWidth) {
// line is not visible
continue;
}
// Y-coordinate of line
int y = fTextWidget.getLinePixel(line);
// compute first visible char offset
int startOffset;
try {
startOffset = fTextWidget.getOffsetAtLocation(new Point(x, y)) - 1;
if (startOffset - 2 <= lineOffset) {
startOffset = lineOffset;
}
} catch (IllegalArgumentException iae) {
// Indicates that there is no character at the given line, should not be logged.
startOffset = lineOffset;
}
// compute last visible char offset
int endOffset;
if (x + w >= endOfLine.x) {
// line end is visible
endOffset = lineEndOffset;
} else {
try {
endOffset = fTextWidget.getOffsetAtLocation(new Point(x + w - 1, y)) + 1;
if (endOffset + 2 >= lineEndOffset) {
endOffset = lineEndOffset;
}
} catch (IllegalArgumentException iae) {
AcceleoUIActivator.log(iae, true);
endOffset = lineEndOffset;
}
}
// draw character range
if (endOffset > startOffset) {
drawCharRange(gc, startOffset, endOffset);
}
}
}
/**
* Draw characters of content range.
*
* @param gc
* the GC
* @param startOffset
* inclusive start index
* @param endOffset
* exclusive end index
*/
private void drawCharRange(GC gc, int startOffset, int endOffset) {
StyledTextContent content = fTextWidget.getContent();
int length = endOffset - startOffset;
String text = content.getTextRange(startOffset, length);
StringBuffer visibleChar = new StringBuffer(10);
for (int textOffset = 0; textOffset <= length; ++textOffset) {
int delta = 0;
boolean eol = false;
if (textOffset < length) {
delta = 1;
char c = text.charAt(textOffset);
boolean shouldDraw = this.shouldDraw(startOffset + textOffset);
if (shouldDraw) {
switch (c) {
case ' ':
visibleChar.append(SPACE_SIGN);
// 'continue' would improve performance but may produce drawing errors
// for long runs of space if width of space and dot differ
break;
case '\u3000': // ideographic whitespace
visibleChar.append(IDEOGRAPHIC_SPACE_SIGN);
// 'continue' would improve performance but may produce drawing errors
// for long runs of space if width of space and dot differ
break;
case '\t':
visibleChar.append(TAB_SIGN);
break;
case '\r':
visibleChar.append(CARRIAGE_RETURN_SIGN);
if (textOffset >= length - 1 || text.charAt(textOffset + 1) != '\n') {
eol = true;
break;
}
continue;
case '\n':
visibleChar.append(LINE_FEED_SIGN);
eol = true;
break;
default:
delta = 0;
break;
}
}
}
if (visibleChar.length() > 0) {
int widgetOffset = startOffset + textOffset - visibleChar.length() + delta;
if (!eol || !isFoldedLine(content.getLineAtOffset(widgetOffset))) {
this.drawCharacter(widgetOffset, gc, visibleChar);
}
visibleChar.delete(0, visibleChar.length());
}
}
}
/**
* Draw the visible char at the given widget offset.
*
* @param widgetOffset
* The widget offset
* @param gc
* The GC
* @param visibleChar
* The visible char to draw
*/
private void drawCharacter(int widgetOffset, GC gc, StringBuffer visibleChar) {
Color fg = null;
StyleRange styleRange = null;
/*
* Block selection is drawn using alpha and no selection-inverting takes place, we always draw as
* 'unselected' in block selection mode.
*/
// Acceleo : compatibility with 3.4 breaks when using this, commenting out
if (/* !fTextWidget.getBlockSelection() && */isOffsetSelected(fTextWidget, widgetOffset)) {
fg = fTextWidget.getSelectionForeground();
} else {
styleRange = fTextWidget.getStyleRangeAtOffset(widgetOffset);
if (styleRange == null || styleRange.foreground == null) {
fg = fTextWidget.getForeground();
} else {
fg = styleRange.foreground;
}
}
draw(gc, widgetOffset, visibleChar.toString(), fg);
}
/**
* Indicates if we should draw the character at the given offset.
*
* @param offset
* The given offset
* @return true if we should draw it, false otherwise
*/
private boolean shouldDraw(int offset) {
ASTNode astNode = this.fEditor.getContent().getASTNode(offset, offset);
return astNode instanceof StringLiteralExp;
}
/**
* Returns <code>true</code> if <code>offset</code> is selection in <code>widget</code>,
* <code>false</code> otherwise.
*
* @param widget
* the widget
* @param offset
* the offset
* @return <code>true</code> if <code>offset</code> is selection, <code>false</code> otherwise
* @since 3.5
*/
private static boolean isOffsetSelected(StyledText widget, int offset) {
Point selection = widget.getSelection();
return offset >= selection.x && offset < selection.y;
}
/**
* Check if the given widget line is a folded line.
*
* @param widgetLine
* the widget line number
* @return <code>true</code> if the line is folded
*/
private boolean isFoldedLine(int widgetLine) {
if (fTextViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5)fTextViewer;
int modelLine = extension.widgetLine2ModelLine(widgetLine);
int widgetLine2 = extension.modelLine2WidgetLine(modelLine + 1);
return widgetLine2 == -1;
}
return false;
}
/**
* Redraw all of the text widgets visible content.
*/
private void redrawAll() {
fTextWidget.redraw();
}
/**
* Draw string at widget offset.
*
* @param gc
* the GC
* @param offset
* the widget offset
* @param s
* the string to be drawn
* @param fg
* the foreground color
*/
private void draw(GC gc, int offset, String s, Color fg) {
// Compute baseline delta (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=165640)
int baseline = fTextWidget.getBaseline(offset);
FontMetrics fontMetrics = gc.getFontMetrics();
int fontBaseline = fontMetrics.getAscent() + fontMetrics.getLeading();
int baslineDelta = baseline - fontBaseline;
Point pos = fTextWidget.getLocationAtOffset(offset);
gc.setForeground(fg);
gc.drawString(s, pos.x, pos.y + baslineDelta, true);
}
/**
* Convert a document offset to the corresponding widget offset.
*
* @param documentOffset
* the document offset
* @return widget offset
*/
private int getWidgetOffset(int documentOffset) {
int result = -1;
if (fTextViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5)fTextViewer;
result = extension.modelOffset2WidgetOffset(documentOffset);
} else {
IRegion visible = fTextViewer.getVisibleRegion();
int widgetOffset = documentOffset - visible.getOffset();
if (widgetOffset > visible.getLength()) {
result = -1;
} else {
result = widgetOffset;
}
}
return result;
}
/**
* Convert a widget offset to the corresponding document offset.
*
* @param widgetOffset
* the widget offset
* @return document offset
*/
private int getDocumentOffset(int widgetOffset) {
int result = -1;
if (fTextViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5)fTextViewer;
result = extension.widgetOffset2ModelOffset(widgetOffset);
} else {
IRegion visible = fTextViewer.getVisibleRegion();
if (widgetOffset > visible.getLength()) {
result = -1;
} else {
result = widgetOffset + visible.getOffset();
}
}
return result;
}
}