| /** |
| * 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.events.ControlEvent; |
| import org.eclipse.swt.events.ControlListener; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseListener; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Device; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.FontData; |
| import org.eclipse.swt.graphics.GlyphMetrics; |
| 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.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 { |
| |
| /** |
| * 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$ |
| |
| /** |
| * The StyledText font normal, bold, italic and bold itlaic both. |
| */ |
| private Font regularFont, boldFont, italicFont, boldItalicFont; |
| |
| /** |
| * 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; |
| StyleRange style= InlinedAnnotationDrawingStrategy.updateStyle(ann, null); |
| if (style != null) { |
| textPresentation.mergeStyleRange(style); |
| } |
| } else if (annotation instanceof LineHeaderAnnotation) { |
| LineHeaderAnnotation ann= (LineHeaderAnnotation) annotation; |
| StyleRange style= InlinedAnnotationDrawingStrategy.updateStyle(ann, null); |
| if (style != null) { |
| textPresentation.mergeStyleRange(style); |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * 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, ControlListener { |
| |
| private int startOffset; |
| |
| private Integer endOffset; |
| |
| public VisibleLines() { |
| install(); |
| fViewer.getTextWidget().getDisplay().asyncExec(() -> { |
| compute(); |
| }); |
| } |
| |
| @Override |
| public void viewportChanged(int verticalOffset) { |
| compute(); |
| } |
| |
| @Override |
| public void documentAboutToBeChanged(DocumentEvent event) { |
| endOffset= null; |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| // Do nothing |
| } |
| |
| @Override |
| public void controlMoved(ControlEvent e) { |
| // Do nothing |
| } |
| |
| @Override |
| public void controlResized(ControlEvent e) { |
| compute(); |
| } |
| |
| @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) { |
| Display display= fViewer.getTextWidget().getDisplay(); |
| if (display.getThread() == Thread.currentThread()) { |
| endOffset= getExclusiveBottomIndexEndOffset(); |
| } else { |
| display.syncExec(() -> endOffset= getExclusiveBottomIndexEndOffset()); |
| } |
| } |
| return offset >= startOffset && offset <= endOffset; |
| } |
| |
| /** |
| * Uninstall visible lines |
| */ |
| void uninstall() { |
| if (fViewer != null) { |
| fViewer.removeViewportListener(this); |
| if (fViewer.getDocument() != null) { |
| fViewer.getDocument().removeDocumentListener(this); |
| } |
| if (fViewer.getTextWidget() != null) { |
| fViewer.getTextWidget().removeControlListener(this); |
| } |
| } |
| } |
| |
| void install() { |
| fViewer.addViewportListener(this); |
| fViewer.getDocument().addDocumentListener(this); |
| fViewer.getTextWidget().addControlListener(this); |
| } |
| } |
| |
| private class MouseTracker implements MouseMoveListener, MouseListener { |
| |
| private AbstractInlinedAnnotation fAnnotation; |
| |
| private Consumer<MouseEvent> fAction; |
| |
| private void update(MouseEvent e) { |
| fAnnotation= null; |
| fAction= null; |
| AbstractInlinedAnnotation annotation= getInlinedAnnotationAtPoint(fViewer, e.x, e.y); |
| if (annotation != null) { |
| Consumer<MouseEvent> action= annotation.getAction(e); |
| if (action != null) { |
| fAnnotation= annotation; |
| fAction= action; |
| } |
| } |
| } |
| |
| @Override |
| public void mouseMove(MouseEvent e) { |
| AbstractInlinedAnnotation oldAnnotation= fAnnotation; |
| update(e); |
| if (oldAnnotation != null) { |
| if (oldAnnotation.equals(fAnnotation)) { |
| // Same annotations which was hovered, do nothing. |
| return; |
| } else { |
| oldAnnotation.onMouseOut(e); |
| } |
| } |
| if (fAnnotation != null) { |
| fAnnotation.onMouseHover(e); |
| } |
| } |
| |
| @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); |
| } |
| } |
| } |
| |
| /** |
| * 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(); |
| text.addMouseListener(fMouseTracker); |
| text.addMouseMoveListener(fMouseTracker); |
| 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.removeMouseMoveListener(this.fMouseTracker); |
| } |
| if (fViewer != null) { |
| if (fViewer instanceof ITextViewerExtension4) { |
| ((ITextViewerExtension4) fViewer).removeTextPresentationListener(updateStylesWidth); |
| } |
| } |
| if (visibleLines != null) { |
| visibleLines.uninstall(); |
| visibleLines= null; |
| } |
| removeInlinedAnnotations(); |
| disposeFont(); |
| 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 {@link AbstractInlinedAnnotation} from the given point and null otherwise. |
| * |
| * @param viewer the source viewer |
| * @param x the x coordinate of the point |
| * @param y the y coordinate of the point |
| * @return the {@link AbstractInlinedAnnotation} from the given point and null otherwise. |
| */ |
| private AbstractInlinedAnnotation getInlinedAnnotationAtPoint(ISourceViewer viewer, int x, int y) { |
| if (fInlinedAnnotations != null) { |
| for (AbstractInlinedAnnotation ann : fInlinedAnnotations) { |
| if (ann.contains(x, y) && isInVisibleLines(ann.getPosition().getOffset())) { |
| return 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) { |
| if (visibleLines == null) { |
| // case of support has been uninstalled and mining must be drawn. |
| return false; |
| } |
| return visibleLines.isInVisibleLines(offset); |
| } |
| |
| /** |
| * Returns the font according the specified <code>style</code> that the receiver will use to |
| * paint textual information. |
| * |
| * @param style the style of Font widget to get. |
| * @return the receiver's font according the specified <code>style</code> |
| * |
| */ |
| Font getFont(int style) { |
| StyledText styledText= fViewer != null ? fViewer.getTextWidget() : null; |
| if (styledText == null) { |
| return null; |
| } |
| if (!styledText.getFont().equals(regularFont)) { |
| disposeFont(); |
| regularFont= styledText.getFont(); |
| } |
| Device device= styledText.getDisplay(); |
| switch (style) { |
| case SWT.BOLD: |
| if (boldFont != null) |
| return boldFont; |
| return boldFont= new Font(device, getFontData(style)); |
| case SWT.ITALIC: |
| if (italicFont != null) |
| return italicFont; |
| return italicFont= new Font(device, getFontData(style)); |
| case SWT.BOLD | SWT.ITALIC: |
| if (boldItalicFont != null) |
| return boldItalicFont; |
| return boldItalicFont= new Font(device, getFontData(style)); |
| default: |
| return regularFont; |
| } |
| } |
| |
| /** |
| * Returns the font data array according the given style. |
| * |
| * @param style the style |
| * @return the font data array according the given style. |
| */ |
| FontData[] getFontData(int style) { |
| FontData[] fontDatas= regularFont.getFontData(); |
| for (int i= 0; i < fontDatas.length; i++) { |
| fontDatas[i].setStyle(style); |
| } |
| return fontDatas; |
| } |
| |
| /** |
| * Dispose the font. |
| */ |
| void disposeFont() { |
| if (boldFont != null) |
| boldFont.dispose(); |
| if (italicFont != null) |
| italicFont.dispose(); |
| if (boldItalicFont != null) |
| boldItalicFont.dispose(); |
| boldFont= italicFont= boldItalicFont= null; |
| } |
| } |