/******************************************************************************* * Copyright (c) 2009, 2016 Atlassian 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: * Atlassian - initial API and implementation * Tasktop Technologies - improvements * Guy Perron 423242: Add ability to edit comment from compare navigator popup ******************************************************************************/ package org.eclipse.mylyn.internal.reviews.ui.annotations; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EContentAdapter; 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.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationModelEvent; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IAnnotationModelListener; import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; import org.eclipse.mylyn.commons.core.StatusHandler; import org.eclipse.mylyn.internal.reviews.ui.ReviewsUiPlugin; import org.eclipse.mylyn.reviews.core.model.IComment; import org.eclipse.mylyn.reviews.core.model.ILineLocation; import org.eclipse.mylyn.reviews.core.model.ILocation; import org.eclipse.mylyn.reviews.core.model.IReviewItem; import org.eclipse.mylyn.reviews.ui.ReviewBehavior; import com.google.common.collect.Iterables; /** * A model for review annotations. * * @author Shawn Minto * @author Steffen Pingel * @author Guy Perron */ public class ReviewAnnotationModel implements IAnnotationModel { private final Set<IAnnotationModelListener> annotationModelListeners = new HashSet<IAnnotationModelListener>(2); private final Set<Annotation> annotations = new LinkedHashSet<Annotation>(); private ReviewBehavior behavior; private IDocument document; private final IDocumentListener documentListener = new IDocumentListener() { public void documentAboutToBeChanged(DocumentEvent event) { } public void documentChanged(DocumentEvent event) { // TODO consider hiding annotations if the document changes //updateAnnotations(false); } }; private final EContentAdapter modelAdapter = new EContentAdapter() { @Override public void notifyChanged(Notification notification) { if (document == null) { return; } if (notification.getEventType() == Notification.ADD) { AnnotationModelEvent event = new AnnotationModelEvent(ReviewAnnotationModel.this); if (notification.getNewValue() instanceof IComment) { createCommentAnnotations(document, event, (IComment) notification.getNewValue()); } fireModelChanged(event); } if (notification.getEventType() == Notification.REMOVE) { AnnotationModelEvent event = new AnnotationModelEvent(ReviewAnnotationModel.this); if (notification.getOldValue() instanceof IComment) { removeCommentAnnotations(document, event, (IComment) notification.getOldValue()); } updateAnnotations(); } if (notification.getEventType() == Notification.SET) { AnnotationModelEvent event = new AnnotationModelEvent(ReviewAnnotationModel.this); if (notification.getNewValue() instanceof IComment) { modifyCommentAnnotations(document, event, (IComment) notification.getOldValue(), (IComment) notification.getNewValue()); } updateAnnotations(); } } }; private IReviewItem reviewItem; public ReviewAnnotationModel() { } public void addAnnotation(Annotation annotation, Position position) { // do nothing, we do not support external modification } public void addAnnotationModelListener(IAnnotationModelListener listener) { if (annotationModelListeners.add(listener)) { fireModelChanged(new AnnotationModelEvent(this, true)); } } public void connect(final IDocument document) { this.document = document; connectItem(); for (Annotation commentAnnotation : annotations) { try { if (commentAnnotation instanceof CommentAnnotation) { document.addPosition(((CommentAnnotation) commentAnnotation).getPosition()); } } catch (BadLocationException e) { StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, e.getMessage(), e)); } } document.addDocumentListener(documentListener); updateAnnotations(); } public void disconnect(IDocument document) { for (Annotation commentAnnotation : annotations) { if (commentAnnotation instanceof CommentAnnotation) { document.removePosition(((CommentAnnotation) commentAnnotation).getPosition()); } } document.removeDocumentListener(documentListener); disconnectItem(); this.document = null; } public void disconnectItem() { if (reviewItem != null) { ((EObject) reviewItem).eAdapters().remove(modelAdapter); } clear(); } public Iterator<Annotation> getAnnotationIterator() { return annotations.iterator(); } /** * Returns the first annotation that this knows about for the given offset in the document */ public List<CommentAnnotation> getAnnotationsForOffset(int offset) { List<CommentAnnotation> result = new ArrayList<CommentAnnotation>(); for (CommentAnnotation annotation : Iterables.filter(this.annotations, CommentAnnotation.class)) { if (annotation.getPosition().offset <= offset && (annotation.getPosition().length + annotation.getPosition().offset) >= offset) { result.add(annotation); } } return result; } public ReviewBehavior getBehavior() { return behavior; } public IDocument getDocument() { return document; } /** * Returns the first annotation that this knows about for the given offset in the document */ public Annotation getFirstAnnotationForOffset(int offset) { for (CommentAnnotation annotation : Iterables.filter(annotations, CommentAnnotation.class)) { if (annotation.getPosition().offset <= offset && (annotation.getPosition().length + annotation.getPosition().offset) >= offset) { return annotation; } } return null; } public IReviewItem getItem() { return reviewItem; } public Position getPosition(Annotation annotation) { if (annotation instanceof CommentAnnotation) { return ((CommentAnnotation) annotation).getPosition(); } else { // we dont understand any other annotations return null; } } public void removeAnnotation(Annotation annotation) { // do nothing, we do not support external modification } public void removeAnnotationModelListener(IAnnotationModelListener listener) { annotationModelListeners.remove(listener); } public void setItem(IReviewItem reviewItem, ReviewBehavior behavior) { disconnectItem(); this.reviewItem = reviewItem; this.behavior = behavior; connectItem(); } private void connectItem() { if (reviewItem != null) { ((EObject) reviewItem).eAdapters().add(modelAdapter); } } private void createCommentAnnotations(IDocument document, AnnotationModelEvent event, IComment comment) { //TODO We need to ensure that this works properly with cases where 0 or many locations exist. for (ILocation location : comment.getLocations()) { if (location instanceof ILineLocation) { try { CommentAnnotation ca = createCommentAnnotation(document, comment, (ILineLocation) location); if (annotations.add(ca)) { event.annotationAdded(ca); } } catch (BadLocationException e) { StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Unable to add annotation.", //$NON-NLS-1$ e)); } } } } private void removeCommentAnnotations(IDocument document, AnnotationModelEvent event, IComment comment) { for (ILocation location : comment.getLocations()) { if (location instanceof ILineLocation) { try { CommentAnnotation ca = createCommentAnnotation(document, comment, (ILineLocation) location); annotations.remove(ca); reviewItem.getComments().remove(comment); event.annotationRemoved(ca); } catch (BadLocationException e) { StatusHandler.log( new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Unable to remove annotation.", e)); //$NON-NLS-1$ } } } } private void modifyCommentAnnotations(IDocument document, AnnotationModelEvent event, IComment oldcomment, IComment comment) { for (ILocation location : comment.getLocations()) { if (location instanceof ILineLocation) { try { CommentAnnotation oldCa = createCommentAnnotation(document, oldcomment, (ILineLocation) location); CommentAnnotation ca = new CommentAnnotation(oldCa.getPosition().offset, oldCa.getPosition().length, comment); annotations.remove(oldCa); annotations.add(ca); event.annotationRemoved(oldCa); event.annotationChanged(ca); } catch (BadLocationException e) { StatusHandler.log( new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Unable to modify annotation.", e)); //$NON-NLS-1$ } } } } private CommentAnnotation createCommentAnnotation(IDocument document, IComment comment, ILineLocation lineLocation) throws BadLocationException { int startLine = lineLocation.getRangeMin(); int endLine = lineLocation.getRangeMax(); int offset = 0; int length = 1; if (startLine != 0 && startLine <= document.getNumberOfLines()) { offset = document.getLineOffset(startLine - 1); if (endLine == 0) { endLine = startLine; } length = Math.max(document.getLineOffset(endLine - 1) - offset, 1); } return new CommentAnnotation(offset, length, comment); } protected void clear() { AnnotationModelEvent event = new AnnotationModelEvent(this); clear(event); fireModelChanged(event); } protected void clear(AnnotationModelEvent event) { for (Annotation commentAnnotation : annotations) { if (commentAnnotation instanceof CommentAnnotation) { event.annotationRemoved(commentAnnotation, ((CommentAnnotation) commentAnnotation).getPosition()); } } annotations.clear(); } protected void fireModelChanged(AnnotationModelEvent event) { event.markSealed(); if (!event.isEmpty()) { for (IAnnotationModelListener listener : annotationModelListeners) { if (listener instanceof IAnnotationModelListenerExtension) { ((IAnnotationModelListenerExtension) listener).modelChanged(event); } else { listener.modelChanged(this); } } } } protected void updateAnnotations() { AnnotationModelEvent event = new AnnotationModelEvent(this); clear(event); if (document != null && reviewItem != null) { for (IComment comment : reviewItem.getComments()) { createCommentAnnotations(document, event, comment); } } fireModelChanged(event); } }