| /** |
| * Copyright (c) 2017, 2018 Angelo ZERR. |
| * 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: |
| * Angelo Zerr <angelo.zerr@gmail.com> - [CodeMining] Provide inline annotations support - Bug 527675 |
| */ |
| package org.eclipse.jface.text.source.inlined; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyleRange; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.custom.StyledTextLineSpacingProvider; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseListener; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.MouseTrackListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.GlyphMetrics; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.widgets.Display; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ISynchronizable; |
| import org.eclipse.jface.text.ITextPresentationListener; |
| import org.eclipse.jface.text.ITextViewerExtension4; |
| import org.eclipse.jface.text.ITextViewerExtension5; |
| import org.eclipse.jface.text.IViewportListener; |
| import org.eclipse.jface.text.JFaceTextUtil; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextPresentation; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.AnnotationPainter; |
| import org.eclipse.jface.text.source.AnnotationPainter.IDrawingStrategy; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension2; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| |
| /** |
| * Support to draw inlined annotations: |
| * |
| * <ul> |
| * <li>line header annotation with {@link LineHeaderAnnotation}.</li> |
| * <li>line content annotation with {@link LineContentAnnotation}.</li> |
| * </ul> |
| * |
| * @since 3.13 |
| */ |
| public class InlinedAnnotationSupport implements StyledTextLineSpacingProvider { |
| |
| /** |
| * The annotation inlined strategy singleton. |
| */ |
| private static final IDrawingStrategy INLINED_STRATEGY= new InlinedAnnotationDrawingStrategy(); |
| |
| /** |
| * The annotation inlined strategy ID. |
| */ |
| private static final String INLINED_STRATEGY_ID= "inlined"; //$NON-NLS-1$ |
| |
| /** |
| * Listener used to update {@link GlyphMetrics} width style for {@link LineContentAnnotation}. |
| */ |
| private ITextPresentationListener updateStylesWidth; |
| |
| /** |
| * Class to update {@link GlyphMetrics} width style for {@link LineContentAnnotation}. |
| * |
| */ |
| private class UpdateStylesWidth implements ITextPresentationListener { |
| |
| @Override |
| public void applyTextPresentation(TextPresentation textPresentation) { |
| IAnnotationModel annotationModel= fViewer.getAnnotationModel(); |
| IRegion region= textPresentation.getExtent(); |
| ((IAnnotationModelExtension2) annotationModel) |
| .getAnnotationIterator(region.getOffset(), region.getLength(), true, true) |
| .forEachRemaining(annotation -> { |
| if (annotation instanceof LineContentAnnotation) { |
| LineContentAnnotation ann= (LineContentAnnotation) annotation; |
| Position position= ann.getPosition(); |
| if (position != null) { |
| int width= ann.getWidth(); |
| if (width != 0) { |
| StyleRange s= new StyleRange(); |
| s.start= position.getOffset(); |
| s.length= 1; |
| s.metrics= ann.isMarkedDeleted() |
| ? null |
| : new GlyphMetrics(0, 0, width); |
| textPresentation.mergeStyleRange(s); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Tracker of start/end offset of visible lines. |
| */ |
| private VisibleLines visibleLines; |
| |
| /** |
| * Class to track start/end offset of visible lines. |
| * |
| */ |
| private class VisibleLines implements IViewportListener, IDocumentListener { |
| |
| private int startOffset; |
| |
| private Integer endOffset; |
| |
| public VisibleLines() { |
| fViewer.getTextWidget().getDisplay().asyncExec(() -> { |
| compute(); |
| }); |
| fViewer.getDocument().addDocumentListener(this); |
| } |
| |
| @Override |
| public void viewportChanged(int verticalOffset) { |
| compute(); |
| } |
| |
| @Override |
| public void documentAboutToBeChanged(DocumentEvent event) { |
| endOffset= null; |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| // Do nothing |
| } |
| |
| @SuppressWarnings("boxing") |
| private void compute() { |
| startOffset= getInclusiveTopIndexStartOffset(); |
| endOffset= getExclusiveBottomIndexEndOffset(); |
| } |
| |
| /** |
| * Returns the document offset of the upper left corner of the source viewer's view port, |
| * possibly including partially visible lines. |
| * |
| * @return the document offset if the upper left corner of the view port |
| */ |
| private int getInclusiveTopIndexStartOffset() { |
| if (fViewer != null && fViewer.getTextWidget() != null && !fViewer.getTextWidget().isDisposed()) { |
| int top= JFaceTextUtil.getPartialTopIndex(fViewer); |
| try { |
| IDocument document= fViewer.getDocument(); |
| return document.getLineOffset(top); |
| } catch (BadLocationException x) { |
| // Do nothing |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Returns the first invisible document offset of the lower right corner of the source |
| * viewer's view port, possibly including partially visible lines. |
| * |
| * @return the first invisible document offset of the lower right corner of the view port |
| */ |
| private int getExclusiveBottomIndexEndOffset() { |
| if (fViewer != null && fViewer.getTextWidget() != null && !fViewer.getTextWidget().isDisposed()) { |
| int bottom= JFaceTextUtil.getPartialBottomIndex(fViewer); |
| try { |
| IDocument document= fViewer.getDocument(); |
| if (bottom >= document.getNumberOfLines()) { |
| bottom= document.getNumberOfLines() - 1; |
| } |
| return document.getLineOffset(bottom) + document.getLineLength(bottom); |
| } catch (BadLocationException x) { |
| // Do nothing |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Return whether the given offset is in visible lines. |
| * |
| * @param offset the offset |
| * @return <code>true</code> if the given offset is in visible lines and <code>false</code> |
| * otherwise. |
| */ |
| @SuppressWarnings("boxing") |
| boolean isInVisibleLines(int offset) { |
| if (endOffset == null) { |
| endOffset= getExclusiveBottomIndexEndOffset(); |
| } |
| return offset >= startOffset && offset <= endOffset; |
| } |
| |
| /** |
| * Uninstall visible lines |
| */ |
| void uninstall() { |
| if (fViewer != null && fViewer.getDocument() != null) { |
| fViewer.getDocument().removeDocumentListener(this); |
| } |
| } |
| } |
| |
| private class MouseTracker implements MouseTrackListener, MouseMoveListener, MouseListener { |
| |
| private AbstractInlinedAnnotation fAnnotation; |
| |
| private Consumer<MouseEvent> fAction; |
| |
| private void update(MouseEvent e) { |
| fAnnotation= null; |
| fAction= null; |
| AbstractInlinedAnnotation annotation= getInlinedAnnotationAtPoint(fViewer, new Point(e.x, e.y)); |
| if (annotation != null) { |
| Consumer<MouseEvent> action= annotation.getAction(e); |
| if (action != null) { |
| fAnnotation= annotation; |
| fAction= action; |
| } |
| } |
| } |
| |
| @Override |
| public void mouseHover(MouseEvent e) { |
| update(e); |
| if (fAnnotation != null) { |
| fAnnotation.onMouseHover(e); |
| } |
| } |
| |
| @Override |
| public void mouseMove(MouseEvent e) { |
| if (fAnnotation != null) { |
| AbstractInlinedAnnotation oldAnnotation= fAnnotation; |
| update(e); |
| if (!oldAnnotation.equals(fAnnotation)) { |
| oldAnnotation.onMouseOut(e); |
| fAnnotation= null; |
| fAction= null; |
| } |
| } |
| } |
| |
| @Override |
| public void mouseDoubleClick(MouseEvent e) { |
| // Do nothing |
| } |
| |
| @Override |
| public void mouseDown(MouseEvent e) { |
| // Do nothing |
| } |
| |
| @Override |
| public void mouseUp(MouseEvent e) { |
| if (e.button != 1) { |
| return; |
| } |
| if (fAction != null) { |
| fAction.accept(e); |
| } |
| } |
| |
| @Override |
| public void mouseEnter(MouseEvent e) { |
| // Do nothing |
| } |
| |
| @Override |
| public void mouseExit(MouseEvent e) { |
| // Do nothing |
| } |
| |
| } |
| |
| /** |
| * The source viewer |
| */ |
| private ISourceViewer fViewer; |
| |
| /** |
| * The annotation painter to use to draw the inlined annotations. |
| */ |
| private AnnotationPainter fPainter; |
| |
| /** |
| * Holds the current inlined annotations. |
| */ |
| private Set<AbstractInlinedAnnotation> fInlinedAnnotations; |
| |
| /** |
| * The mouse tracker used to support hover, click on inlined annotation. |
| */ |
| private final MouseTracker fMouseTracker= new MouseTracker(); |
| |
| /** |
| * Install the inlined annotation support for the given viewer. |
| * |
| * @param viewer the source viewer |
| * @param painter the annotation painter to use to draw the inlined annotations. |
| */ |
| public void install(ISourceViewer viewer, AnnotationPainter painter) { |
| Assert.isNotNull(viewer); |
| Assert.isNotNull(painter); |
| fViewer= viewer; |
| fPainter= painter; |
| initPainter(); |
| StyledText text= fViewer.getTextWidget(); |
| if (text == null || text.isDisposed()) { |
| return; |
| } |
| if (fViewer instanceof ITextViewerExtension4) { |
| updateStylesWidth= new UpdateStylesWidth(); |
| ((ITextViewerExtension4) fViewer).addTextPresentationListener(updateStylesWidth); |
| } |
| visibleLines= new VisibleLines(); |
| fViewer.addViewportListener(visibleLines); |
| text.addMouseListener(fMouseTracker); |
| text.addMouseTrackListener(fMouseTracker); |
| text.addMouseMoveListener(fMouseTracker); |
| text.setLineSpacingProvider(this); |
| setColor(text.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); |
| } |
| |
| /** |
| * Initialize painter with inlined drawing strategy. |
| */ |
| private void initPainter() { |
| fPainter.addDrawingStrategy(INLINED_STRATEGY_ID, INLINED_STRATEGY); |
| fPainter.addAnnotationType(AbstractInlinedAnnotation.TYPE, INLINED_STRATEGY_ID); |
| } |
| |
| /** |
| * Set the color to use to draw the inlined annotations. |
| * |
| * @param color the color to use to draw the inlined annotations. |
| */ |
| public void setColor(Color color) { |
| fPainter.setAnnotationTypeColor(AbstractInlinedAnnotation.TYPE, color); |
| } |
| |
| /** |
| * Unisntall the inlined annotation support |
| */ |
| public void uninstall() { |
| StyledText text= this.fViewer.getTextWidget(); |
| if (text != null && !text.isDisposed()) { |
| text.removeMouseListener(this.fMouseTracker); |
| text.removeMouseTrackListener(this.fMouseTracker); |
| text.removeMouseMoveListener(this.fMouseTracker); |
| } |
| if (fViewer != null) { |
| if (fViewer instanceof ITextViewerExtension4) { |
| ((ITextViewerExtension4) fViewer).removeTextPresentationListener(updateStylesWidth); |
| } |
| fViewer.removeViewportListener(visibleLines); |
| } |
| if (visibleLines != null) { |
| visibleLines.uninstall(); |
| visibleLines= null; |
| } |
| fViewer= null; |
| fPainter= null; |
| } |
| |
| /** |
| * Update the given inlined annotation. |
| * |
| * @param annotations the inlined annotation. |
| */ |
| public void updateAnnotations(Set<AbstractInlinedAnnotation> annotations) { |
| IDocument document= fViewer != null ? fViewer.getDocument() : null; |
| if (document == null) { |
| // this case comes from when editor is closed before rendered is done. |
| return; |
| } |
| IAnnotationModel annotationModel= fViewer.getAnnotationModel(); |
| if (annotationModel == null) { |
| return; |
| } |
| Map<AbstractInlinedAnnotation, Position> annotationsToAdd= new HashMap<>(); |
| List<AbstractInlinedAnnotation> annotationsToRemove= fInlinedAnnotations != null |
| ? new ArrayList<>(fInlinedAnnotations) |
| : Collections.emptyList(); |
| // Loop for annotations to update |
| for (AbstractInlinedAnnotation ann : annotations) { |
| if (!annotationsToRemove.remove(ann)) { |
| // The annotation was not created, add it |
| annotationsToAdd.put(ann, ann.getPosition()); |
| } |
| } |
| // Process annotations to remove |
| for (AbstractInlinedAnnotation ann : annotationsToRemove) { |
| // Mark annotation as deleted to ignore the draw |
| ann.markDeleted(true); |
| } |
| // Update annotation model |
| synchronized (getLockObject(annotationModel)) { |
| // Update annotations with this inlined annotation support. |
| for (AbstractInlinedAnnotation ann : annotations) { |
| ann.setSupport(this); |
| } |
| if (annotationsToAdd.size() == 0 && annotationsToRemove.size() == 0) { |
| // None change, do nothing. Here the user could change position of codemining |
| // range |
| // (ex: user key press |
| // "Enter"), but we don't need to redraw the viewer because change of position |
| // is done by AnnotationPainter. |
| } else { |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| ((IAnnotationModelExtension) annotationModel).replaceAnnotations( |
| annotationsToRemove.toArray(new Annotation[annotationsToRemove.size()]), annotationsToAdd); |
| } else { |
| removeInlinedAnnotations(); |
| Iterator<Entry<AbstractInlinedAnnotation, Position>> iter= annotationsToAdd.entrySet().iterator(); |
| while (iter.hasNext()) { |
| Entry<AbstractInlinedAnnotation, Position> mapEntry= iter.next(); |
| annotationModel.addAnnotation(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| } |
| } |
| fInlinedAnnotations= annotations; |
| } |
| } |
| |
| /** |
| * Returns the existing codemining annotation with the given position information and null |
| * otherwise. |
| * |
| * @param pos the position |
| * @return the existing codemining annotation with the given position information and null |
| * otherwise. |
| */ |
| @SuppressWarnings("unchecked") |
| public <T extends AbstractInlinedAnnotation> T findExistingAnnotation(Position pos) { |
| if (fInlinedAnnotations == null) { |
| return null; |
| } |
| for (AbstractInlinedAnnotation ann : fInlinedAnnotations) { |
| if (pos.equals(ann.getPosition()) && !ann.getPosition().isDeleted()) { |
| try { |
| return (T) ann; |
| } catch (ClassCastException e) { |
| // Do nothing |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the lock object for the given annotation model. |
| * |
| * @param annotationModel the annotation model |
| * @return the annotation model's lock object |
| */ |
| private Object getLockObject(IAnnotationModel annotationModel) { |
| if (annotationModel instanceof ISynchronizable) { |
| Object lock= ((ISynchronizable) annotationModel).getLockObject(); |
| if (lock != null) |
| return lock; |
| } |
| return annotationModel; |
| } |
| |
| /** |
| * Remove the inlined annotations. |
| */ |
| private void removeInlinedAnnotations() { |
| |
| IAnnotationModel annotationModel= fViewer.getAnnotationModel(); |
| if (annotationModel == null || fInlinedAnnotations == null) |
| return; |
| |
| synchronized (getLockObject(annotationModel)) { |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| ((IAnnotationModelExtension) annotationModel).replaceAnnotations( |
| fInlinedAnnotations.toArray(new Annotation[fInlinedAnnotations.size()]), null); |
| } else { |
| for (AbstractInlinedAnnotation annotation : fInlinedAnnotations) |
| annotationModel.removeAnnotation(annotation); |
| } |
| fInlinedAnnotations= null; |
| } |
| } |
| |
| /** |
| * Returns the line spacing from the given line index with the codemining annotations height and |
| * null otherwise. |
| */ |
| @SuppressWarnings("boxing") |
| @Override |
| public Integer getLineSpacing(int lineIndex) { |
| AbstractInlinedAnnotation annotation= getInlinedAnnotationAtLine(fViewer, lineIndex); |
| return (annotation instanceof LineHeaderAnnotation) |
| ? ((LineHeaderAnnotation) annotation).getHeight() |
| : null; |
| } |
| |
| /** |
| * Returns the {@link AbstractInlinedAnnotation} from the given line index and null otherwise. |
| * |
| * @param viewer the source viewer |
| * @param lineIndex the line index. |
| * @return the {@link AbstractInlinedAnnotation} from the given line index and null otherwise. |
| */ |
| private static AbstractInlinedAnnotation getInlinedAnnotationAtLine(ISourceViewer viewer, int lineIndex) { |
| if (viewer == null) { |
| return null; |
| } |
| IAnnotationModel annotationModel= viewer.getAnnotationModel(); |
| if (annotationModel == null) { |
| return null; |
| } |
| IDocument document= viewer.getDocument(); |
| int lineNumber= lineIndex + 1; |
| if (lineNumber > document.getNumberOfLines()) { |
| return null; |
| } |
| try { |
| if (viewer instanceof ITextViewerExtension5) { |
| lineNumber= ((ITextViewerExtension5) viewer).widgetLine2ModelLine(lineNumber); |
| } |
| IRegion line= document.getLineInformation(lineNumber); |
| return getInlinedAnnotationAtOffset(viewer, line.getOffset(), line.getLength()); |
| } catch (BadLocationException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the {@link AbstractInlinedAnnotation} from the given point and null otherwise. |
| * |
| * @param viewer the source viewer |
| * @param point the origin of character bounding box relative to the origin of the widget client |
| * area. |
| * @return the {@link AbstractInlinedAnnotation} from the given point and null otherwise. |
| */ |
| private static AbstractInlinedAnnotation getInlinedAnnotationAtPoint(ISourceViewer viewer, Point point) { |
| AbstractInlinedAnnotation annotation= getLineContentAnnotationAtPoint(viewer, point); |
| if (annotation != null) { |
| return annotation; |
| } |
| return getLineHeaderAnnotationAtPoint(viewer, point); |
| } |
| |
| /** |
| * Returns the {@link AbstractInlinedAnnotation} line content from the given point and null |
| * otherwise. |
| * |
| * @param viewer the source viewer |
| * @param point the origin of character bounding box relative to the origin of the widget client |
| * area. |
| * @return the {@link AbstractInlinedAnnotation} line content from the given point and null |
| * otherwise. |
| */ |
| private static AbstractInlinedAnnotation getLineContentAnnotationAtPoint(ISourceViewer viewer, Point point) { |
| StyledText styledText= viewer.getTextWidget(); |
| int offset= styledText.getOffsetAtPoint(point); |
| if (offset == -1) { |
| return null; |
| } |
| if (viewer instanceof ITextViewerExtension5) { |
| offset= ((ITextViewerExtension5) viewer).widgetOffset2ModelOffset(offset); |
| } |
| AbstractInlinedAnnotation annotation= getInlinedAnnotationAtOffset(viewer, offset, 1); |
| if (annotation instanceof LineContentAnnotation) { |
| return annotation; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the {@link AbstractInlinedAnnotation} line header from the given point and null |
| * otherwise. |
| * |
| * @param viewer the source viewer |
| * @param point the origin of character bounding box relative to the origin of the widget client |
| * area. |
| * @return the {@link AbstractInlinedAnnotation} line header from the given point and null |
| * otherwise. |
| */ |
| private static AbstractInlinedAnnotation getLineHeaderAnnotationAtPoint(ISourceViewer viewer, Point point) { |
| StyledText styledText= viewer.getTextWidget(); |
| int lineIndex= styledText.getLineIndex(point.y); |
| AbstractInlinedAnnotation annotation= getInlinedAnnotationAtLine(viewer, lineIndex); |
| if (annotation instanceof LineHeaderAnnotation) { |
| return annotation; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the {@link AbstractInlinedAnnotation} from the given offset and null otherwise. |
| * |
| * @param viewer the source viewer |
| * @param offset the start position of the region, must be >= 0 |
| * @param length the length of the region, must be >= 0 |
| * @return the {@link AbstractInlinedAnnotation} from the given offset and null otherwise. |
| */ |
| private static AbstractInlinedAnnotation getInlinedAnnotationAtOffset(ISourceViewer viewer, int offset, int length) { |
| if (viewer == null) { |
| return null; |
| } |
| IAnnotationModel annotationModel= viewer.getAnnotationModel(); |
| if (annotationModel == null) { |
| return null; |
| } |
| Iterator<Annotation> iter= (annotationModel instanceof IAnnotationModelExtension2) |
| ? ((IAnnotationModelExtension2) annotationModel).getAnnotationIterator(offset, |
| length, true, true) |
| : annotationModel.getAnnotationIterator(); |
| while (iter.hasNext()) { |
| Annotation ann= iter.next(); |
| if (ann instanceof AbstractInlinedAnnotation) { |
| Position p= annotationModel.getPosition(ann); |
| if (p != null) { |
| if (p.overlapsWith(offset, length)) { |
| return (AbstractInlinedAnnotation) ann; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Execute UI {@link StyledText} function which requires UI Thread. |
| * |
| * @param text the styled text |
| * @param fn the function to execute. |
| */ |
| static void runInUIThread(StyledText text, Consumer<StyledText> fn) { |
| if (text == null || text.isDisposed()) { |
| return; |
| } |
| Display display= text.getDisplay(); |
| if (display.getThread() == Thread.currentThread()) { |
| try { |
| fn.accept(text); |
| } catch (Exception e) { |
| // Ignore UI error |
| } |
| } else { |
| display.asyncExec(() -> { |
| if (text.isDisposed()) { |
| return; |
| } |
| try { |
| fn.accept(text); |
| } catch (Exception e) { |
| // Ignore UI error |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Return whether the given offset is in visible lines. |
| * |
| * @param offset the offset |
| * @return <code>true</code> if the given offset is in visible lines and <code>false</code> |
| * otherwise. |
| */ |
| boolean isInVisibleLines(int offset) { |
| return visibleLines.isInVisibleLines(offset); |
| } |
| } |