blob: 9947f589f53358ff84647b24814bc0d1b8d6f8e7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009 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 - cleanup and support for gotoAnnotation
* Guy Perron 423242: Add ability to edit comment from compare navigator popup
******************************************************************************/
package org.eclipse.mylyn.internal.reviews.ui.compare;
import static org.eclipse.mylyn.internal.reviews.ui.compare.ReviewCompareAnnotationSupport.Side.LEFT_SIDE;
import static org.eclipse.mylyn.internal.reviews.ui.compare.ReviewCompareAnnotationSupport.Side.RIGHT_SIDE;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
import org.eclipse.compare.internal.MergeSourceViewer;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.LineRange;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.source.projection.AnnotationBag;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.internal.reviews.ui.ReviewsUiPlugin;
import org.eclipse.mylyn.internal.reviews.ui.annotations.CommentAnnotation;
import org.eclipse.mylyn.internal.reviews.ui.annotations.CommentAnnotationHoverInput;
import org.eclipse.mylyn.internal.reviews.ui.annotations.CommentPopupDialog;
import org.eclipse.mylyn.internal.reviews.ui.annotations.ReviewAnnotationModel;
import org.eclipse.mylyn.reviews.core.model.IFileItem;
import org.eclipse.mylyn.reviews.core.model.IReviewItem;
import org.eclipse.mylyn.reviews.ui.ReviewBehavior;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.texteditor.AbstractTextEditor;
/**
* Manages annotation models for compare viewers.
*
* @author Thomas Ehrnhoefer
* @author Steffen Pingel
* @author Guy Perron
*/
@SuppressWarnings("restriction")
public class ReviewCompareAnnotationSupport {
public static enum Side {
LEFT_SIDE, RIGHT_SIDE
}
private static String KEY_ANNOTAION_SUPPORT = ReviewItemSetCompareEditorInput.class.getName();
private CommentPopupDialog commentPopupDialog = null;
public static ReviewCompareAnnotationSupport getAnnotationSupport(Viewer contentViewer) {
ReviewCompareAnnotationSupport support = (ReviewCompareAnnotationSupport) contentViewer.getData(KEY_ANNOTAION_SUPPORT);
if (support == null) {
support = new ReviewCompareAnnotationSupport(contentViewer);
contentViewer.setData(KEY_ANNOTAION_SUPPORT, support);
}
return support;
}
public class MonitorObject {
};
MonitorObject myMonitorObject = new MonitorObject();
private ReviewBehavior behavior;
private final ReviewAnnotationModel leftAnnotationModel;
private ReviewCompareInputListener leftViewerListener;
private final ReviewAnnotationModel rightAnnotationModel;
private ReviewCompareInputListener rightViewerListener;
private MergeSourceViewer leftSourceViewer;
private MergeSourceViewer rightSourceViewer;
public ReviewCompareAnnotationSupport(Viewer contentViewer) {
this.leftAnnotationModel = new ReviewAnnotationModel();
this.rightAnnotationModel = new ReviewAnnotationModel();
install(contentViewer);
contentViewer.setData(KEY_ANNOTAION_SUPPORT, this);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ReviewCompareAnnotationSupport other = (ReviewCompareAnnotationSupport) obj;
if (leftAnnotationModel == null) {
if (other.leftAnnotationModel != null) {
return false;
}
} else if (!leftAnnotationModel.equals(other.leftAnnotationModel)) {
return false;
}
if (rightAnnotationModel == null) {
if (other.rightAnnotationModel != null) {
return false;
}
} else if (!rightAnnotationModel.equals(other.rightAnnotationModel)) {
return false;
}
return true;
}
public ReviewBehavior getBehavior() {
return behavior;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((leftAnnotationModel == null) ? 0 : leftAnnotationModel.hashCode());
result = prime * result + ((rightAnnotationModel == null) ? 0 : rightAnnotationModel.hashCode());
return result;
}
public void install(Viewer contentViewer) {
// FIXME: hack
if (contentViewer instanceof TextMergeViewer) {
TextMergeViewer textMergeViewer = (TextMergeViewer) contentViewer;
try {
Class<TextMergeViewer> clazz = TextMergeViewer.class;
Field declaredField = clazz.getDeclaredField("fLeft"); //$NON-NLS-1$
declaredField.setAccessible(true);
leftSourceViewer = (MergeSourceViewer) declaredField.get(textMergeViewer);
declaredField = clazz.getDeclaredField("fRight"); //$NON-NLS-1$
declaredField.setAccessible(true);
rightSourceViewer = (MergeSourceViewer) declaredField.get(textMergeViewer);
leftViewerListener = registerInputListener(leftSourceViewer, leftAnnotationModel);
rightViewerListener = registerInputListener(rightSourceViewer, rightAnnotationModel);
} catch (Throwable t) {
StatusHandler.log(new Status(IStatus.WARNING, ReviewsUiPlugin.PLUGIN_ID,
"Could not initialize annotation model for " + Viewer.class.getName(), t)); //$NON-NLS-1$
}
}
}
public boolean hasAnnotation(Direction direction) {
Position rightPosition = new Position(0, 0);
return findAnnotation(rightSourceViewer, direction, rightPosition, rightAnnotationModel) != null;
}
/**
* Jumps to the next annotation according to the given direction.
*
* @param direction
* the search direction
* @return the selected annotation or <code>null</code> if none
*/
public Annotation gotoAnnotation(Direction direction) {
if (leftSourceViewer == null) {
return null;
}
int currentLeftOffset = getSelection(leftSourceViewer).getOffset();
Position nextLeftPosition = new Position(0, 0);
Annotation leftAnnotation = findAnnotation(leftSourceViewer, direction, nextLeftPosition, leftAnnotationModel);
Position nextRightPosition = new Position(0, 0);
Annotation rightAnnotation = findAnnotation(rightSourceViewer, direction, nextRightPosition,
rightAnnotationModel);
if (leftAnnotation == null && rightAnnotation != null) {
selectAndReveal(rightSourceViewer, nextRightPosition);
return rightAnnotation;
} else if (leftAnnotation != null && rightAnnotation == null) {
selectAndReveal(leftSourceViewer, nextLeftPosition);
return leftAnnotation;
} else if (leftAnnotation != null && rightAnnotation != null) {
nextLeftPosition.offset = getLineOffset(leftAnnotationModel, nextLeftPosition.offset);
nextLeftPosition.length = 1;
nextRightPosition.offset = getLineOffset(rightAnnotationModel, nextRightPosition.offset);
nextRightPosition.length = 1;
currentLeftOffset = getLineOffset(leftAnnotationModel, currentLeftOffset);
if (calculateNextAnnotation(direction, nextLeftPosition, nextRightPosition, currentLeftOffset) == LEFT_SIDE) {
return leftAnnotation;
} else {
return rightAnnotation;
}
}
return null;
}
private int getLineOffset(ReviewAnnotationModel annotationModel, int offset) {
try {
int line = annotationModel.getDocument().getLineOfOffset(offset);
return annotationModel.getDocument().getLineOffset(line);
} catch (BadLocationException e) {
StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Error displaying comment", e)); //$NON-NLS-1$
}
return 0;
}
public Side calculateNextAnnotation(Direction direction, Position nextLeftPosition, Position nextRightPosition,
Integer currentLeftOffset) {
if (direction == Direction.FORWARDS) {
if (nextLeftPosition.offset == nextRightPosition.offset) {
moveToAnnotation(rightSourceViewer, leftSourceViewer, nextLeftPosition);
rightSourceViewer.getSourceViewer().revealRange(nextLeftPosition.offset - 1,
nextLeftPosition.length - 1);
rightSourceViewer.getSourceViewer().setSelectedRange(nextLeftPosition.offset - 1,
nextLeftPosition.length - 1);
return LEFT_SIDE;
} else if ((nextLeftPosition.offset < currentLeftOffset && nextRightPosition.offset < currentLeftOffset)
|| (nextLeftPosition.offset > currentLeftOffset && nextRightPosition.offset > currentLeftOffset)) {
if ((nextLeftPosition.offset < nextRightPosition.offset)) {
return moveToLeftAnnotation(nextLeftPosition);
} else {
return moveToRightAnnotation(nextRightPosition);
}
} else if ((nextLeftPosition.offset < currentLeftOffset && nextRightPosition.offset > currentLeftOffset)) {
return moveToRightAnnotation(nextRightPosition);
} else if ((nextLeftPosition.offset > currentLeftOffset && nextRightPosition.offset < currentLeftOffset)) {
return moveToLeftAnnotation(nextLeftPosition);
} else if (nextRightPosition.offset == currentLeftOffset) {
return moveToLeftAnnotation(nextLeftPosition);
} else {
return moveToRightAnnotation(nextRightPosition);
}
} else { // backwards
if (nextLeftPosition.offset == nextRightPosition.offset) {
moveToAnnotation(leftSourceViewer, rightSourceViewer, nextRightPosition);
Position position = getNextLine(nextRightPosition.offset);
leftSourceViewer.getSourceViewer().revealRange(position.offset, position.length);
leftSourceViewer.getSourceViewer().setSelectedRange(position.offset, position.length);
return RIGHT_SIDE;
} else if ((nextLeftPosition.offset > currentLeftOffset && nextRightPosition.offset > currentLeftOffset)
|| (nextLeftPosition.offset < currentLeftOffset && nextRightPosition.offset < currentLeftOffset)) {
if ((nextLeftPosition.offset > nextRightPosition.offset)) {
return moveToLeftAnnotation(nextLeftPosition);
} else {
return moveToRightAnnotation(nextRightPosition);
}
} else if ((nextLeftPosition.offset > currentLeftOffset && nextRightPosition.offset < currentLeftOffset)) {
return moveToRightAnnotation(nextRightPosition);
} else if ((nextLeftPosition.offset < currentLeftOffset && nextRightPosition.offset > currentLeftOffset)) {
return moveToLeftAnnotation(nextLeftPosition);
} else if (nextRightPosition.offset == currentLeftOffset) {
return moveToLeftAnnotation(nextLeftPosition);
} else {
return moveToRightAnnotation(nextRightPosition);
}
}
}
private Position getNextLine(int offset) {
Position position = new Position(0, 0);
try {
int line = rightAnnotationModel.getDocument().getLineOfOffset(offset);
IRegion region = rightAnnotationModel.getDocument().getLineInformation(line + 1);
position.offset = region.getOffset();
position.length = region.getLength();
} catch (BadLocationException e) {
StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Error displaying comment", e)); //$NON-NLS-1$
}
return position;
}
private Side moveToLeftAnnotation(Position nextLeftPosition) {
moveToAnnotation(rightSourceViewer, leftSourceViewer, nextLeftPosition);
return LEFT_SIDE;
}
private Side moveToRightAnnotation(Position nextRightPosition) {
moveToAnnotation(leftSourceViewer, rightSourceViewer, nextRightPosition);
return RIGHT_SIDE;
}
public void moveToAnnotation(MergeSourceViewer adjacentViewer, MergeSourceViewer annotationViewer, Position position) {
adjacentViewer.getSourceViewer().revealRange(position.offset, position.length);
adjacentViewer.getSourceViewer().setSelectedRange(position.offset, position.length);
selectAndReveal(annotationViewer, position);
}
// adapted from {@link AbstractTextEditor#selectAndReveal(int, int)}
private void selectAndReveal(MergeSourceViewer sourceViewer, Position position) {
StyledText widget = sourceViewer.getSourceViewer().getTextWidget();
widget.setRedraw(false);
adjustHighlightRange(sourceViewer.getSourceViewer(), position.offset, position.length);
sourceViewer.getSourceViewer().revealRange(position.offset, position.length);
sourceViewer.getSourceViewer().setSelectedRange(position.offset, position.length);
SourceViewer srcViewer = sourceViewer.getSourceViewer();
IReviewItem reviewitem = ((ReviewAnnotationModel) srcViewer.getAnnotationModel()).getItem();
List<CommentAnnotation> comments = getAnnotationsForLine(srcViewer, position.offset);
Point p = sourceViewer.getLineRange(position, sourceViewer.getSourceViewer().getSelectedRange());
LineRange range = new LineRange(p.x + 1, p.y);
if (commentPopupDialog != null) {
commentPopupDialog.dispose();
commentPopupDialog = null;
}
commentPopupDialog = new CommentPopupDialog(ReviewsUiPlugin.getDefault()
.getWorkbench()
.getActiveWorkbenchWindow()
.getShell(), SWT.NO_FOCUS | SWT.ON_TOP, true, reviewitem, range);
CommentAnnotationHoverInput input = new CommentAnnotationHoverInput(comments,
((ReviewAnnotationModel) srcViewer.getAnnotationModel()).getBehavior());
commentPopupDialog.create();
commentPopupDialog.setInput(input);
commentPopupDialog.setSize(50, 150);
Point location = sourceViewer.getSourceViewer().getControl().getLocation();
location = Display.getCurrent().getCursorLocation();
location.y = location.y + (sourceViewer.getViewportHeight() / 2);
commentPopupDialog.setLocation(location);
commentPopupDialog.open();
commentPopupDialog.setFocus();
widget.setRedraw(true);
}
@SuppressWarnings("unchecked")
private List<CommentAnnotation> getAnnotationsForLine(SourceViewer viewer, int offset) {
IAnnotationModel model = viewer.getAnnotationModel();
if (model == null) {
return Collections.emptyList();
}
IDocument document = viewer.getDocument();
int line = 0;
try {
line = document.getLineOfOffset(offset);
} catch (BadLocationException e1) {
StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID, "Error fetching line", e1)); //$NON-NLS-1$
}
List<CommentAnnotation> commentAnnotations = new ArrayList<CommentAnnotation>();
for (Iterator<Annotation> it = model.getAnnotationIterator(); it.hasNext();) {
Annotation annotation = it.next();
Position position = model.getPosition(annotation);
if (position == null || !isPositionOnLine(position, line, document)) {
continue;
}
if (annotation instanceof AnnotationBag) {
AnnotationBag bag = (AnnotationBag) annotation;
Iterator<Annotation> e = bag.iterator();
while (e.hasNext()) {
annotation = e.next();
position = model.getPosition(annotation);
if (position != null && includeAnnotation(annotation, position, commentAnnotations)) {
commentAnnotations.add((CommentAnnotation) annotation);
}
}
} else if (includeAnnotation(annotation, position, commentAnnotations)) {
commentAnnotations.add((CommentAnnotation) annotation);
}
}
return commentAnnotations;
}
private boolean includeAnnotation(Annotation annotation, Position position, List<CommentAnnotation> annotations) {
return annotation instanceof CommentAnnotation && !annotations.contains(annotation);
}
private boolean isPositionOnLine(Position position, int line, IDocument document) {
if (position.getOffset() > -1 && position.getLength() > -1) {
try {
return line == document.getLineOfOffset(position.getOffset());
} catch (BadLocationException x) {
// ignore
}
}
return false;
}
// adapted from {@link AbstractTextEditor#selectAndReveal(int, int)}
protected void adjustHighlightRange(SourceViewer sourceViewer, int offset, int length) {
if (sourceViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5) sourceViewer;
extension.exposeModelRange(new Region(offset, length));
} else if (!isVisible(sourceViewer, offset, length)) {
sourceViewer.resetVisibleRegion();
}
}
// adapted from {@link AbstractTextEditor#selectAndReveal(int, int)}
private boolean isVisible(SourceViewer viewer, int offset, int length) {
if (viewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5) viewer;
IRegion overlap = extension.modelRange2WidgetRange(new Region(offset, length));
return overlap != null;
}
return viewer.overlapsWithVisibleRegion(offset, length);
}
public void setReviewItem(IFileItem item, ReviewBehavior behavior) {
leftAnnotationModel.setItem(item.getBase(), behavior);
rightAnnotationModel.setItem(item.getTarget(), behavior);
Display.getDefault().asyncExec(new Runnable() {
public void run() {
try {
// if listeners exist, just make sure the hover hack is in there
if (leftViewerListener != null) {
leftViewerListener.forceCustomAnnotationHover();
}
if (rightViewerListener != null) {
rightViewerListener.forceCustomAnnotationHover();
}
} catch (Exception e) {
StatusHandler.log(new Status(IStatus.ERROR, ReviewsUiPlugin.PLUGIN_ID,
"Error attaching annotation hover", e)); //$NON-NLS-1$
}
}
});
}
private ReviewCompareInputListener registerInputListener(final MergeSourceViewer sourceViewer,
final ReviewAnnotationModel annotationModel) {
ReviewCompareInputListener listener = new ReviewCompareInputListener(sourceViewer, annotationModel);
SourceViewer viewer = CompareUtil.getSourceViewer(sourceViewer);
if (viewer != null) {
viewer.addTextInputListener(listener);
}
listener.registerContextMenu();
return listener;
}
/**
* Returns the annotation closest to the given range respecting the given direction. If an annotation is found, the
* annotations current position is copied into the provided annotation position.
*
* @param viewer
* the viewer
* @param direction
* the search direction
* @param annotationPosition
* the position of the found annotation
* @param annotationModel
* the annotation model to process
* @return the found annotation
* @see borrowed and adapted from {@link AbstractTextEditor}
*/
@SuppressWarnings("null")
protected Annotation findAnnotation(MergeSourceViewer viewer, Direction direction, Position annotationPosition,
ReviewAnnotationModel annotationModel) {
if (viewer == null) {
return null;
}
ITextSelection selection = getSelection(viewer);
final int offset = selection.getOffset();
final int length = selection.getLength();
Annotation nextAnnotation = null;
Position nextAnnotationPosition = null;
Annotation containingAnnotation = null;
Position containingAnnotationPosition = null;
boolean currentAnnotation = false;
IDocument document = annotationModel.getDocument();
if (document == null) {
return null;
}
int endOfDocument = document.getLength();
int distance = Integer.MAX_VALUE;
Iterator<CommentAnnotation> e = annotationModel.getAnnotationIterator();
while (e.hasNext()) {
CommentAnnotation a = e.next();
Position p = a.getPosition();
if (p == null) {
continue;
}
if (direction == Direction.FORWARDS && p.offset == offset || direction == Direction.BACKWARDS
&& p.offset + p.getLength() == offset + length) {// || p.includes(offset)) {
if (containingAnnotation == null
|| (direction == Direction.FORWARDS && p.length >= containingAnnotationPosition.length || direction == Direction.BACKWARDS
&& p.length >= containingAnnotationPosition.length)) {
containingAnnotation = a;
containingAnnotationPosition = p;
currentAnnotation = p.length == length;
}
} else {
int currentDistance = 0;
if (direction == Direction.FORWARDS) {
currentDistance = p.getOffset() - offset;
if (currentDistance < 0) {
currentDistance = endOfDocument + currentDistance;
}
if (currentDistance < distance || currentDistance == distance
&& p.length < nextAnnotationPosition.length) {
distance = currentDistance;
nextAnnotation = a;
nextAnnotationPosition = p;
}
} else {
currentDistance = offset + length - (p.getOffset() + p.length);
if (currentDistance < 0) {
currentDistance = endOfDocument + currentDistance;
}
if (currentDistance < distance || currentDistance == distance
&& p.length < nextAnnotationPosition.length) {
distance = currentDistance;
nextAnnotation = a;
nextAnnotationPosition = p;
}
}
}
}
if (containingAnnotationPosition != null && (!currentAnnotation || nextAnnotation == null)) {
annotationPosition.setOffset(containingAnnotationPosition.getOffset());
annotationPosition.setLength(containingAnnotationPosition.getLength());
return containingAnnotation;
}
if (nextAnnotationPosition != null) {
annotationPosition.setOffset(nextAnnotationPosition.getOffset());
annotationPosition.setLength(nextAnnotationPosition.getLength());
}
return nextAnnotation;
}
private ITextSelection getSelection(MergeSourceViewer viewer) {
return (ITextSelection) viewer.getSourceViewer().getSelectionProvider().getSelection();
}
}