blob: 805f126a67dfe62d9065044e9669f79b59811b1e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2009 David Green and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.wikitext.ui.viewer;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.resource.ImageDescriptor;
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.ITextInputListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.AnnotationPainter;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.mylyn.internal.wikitext.ui.WikiTextUiPlugin;
import org.eclipse.mylyn.internal.wikitext.ui.util.ImageCache;
import org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.ImageAnnotation;
import org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.ImageDrawingStrategy;
import org.eclipse.mylyn.wikitext.ui.viewer.HtmlViewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
/**
* Manages all aspects of image download/display in an {@link HtmlViewer}. Manages the download of images for viewing in
* an {@link HtmlViewer}, and creates appropriate space for their display. Downloads image data in a background thread,
* instantiates the corresopnding images, and ensures that enough vertical space exists in the viewer to display the
* images.
*
* @see ImageAnnotation
* @see ImageDrawingStrategy
* @see ImageCache
* @see HtmlViewer
* @author David Green
*/
public class ImageManager implements ITextInputListener, DisposeListener, IDocumentListener, ISelectionChangedListener {
private class HyperlinkMouseListener implements MouseMoveListener, MouseListener {
@SuppressWarnings("unused")
public void mouseEnter(MouseEvent e) {
// ignore
}
@SuppressWarnings("unused")
public void mouseExit(MouseEvent e) {
disarm();
}
public void mouseMove(MouseEvent e) {
adjust(e);
}
public void mouseDoubleClick(MouseEvent e) {
// ignore
}
public void mouseDown(MouseEvent e) {
// ignore
}
public void mouseUp(MouseEvent e) {
clicked(e);
}
}
private final HtmlViewer viewer;
private final Display display;
private final ImageCache imageCache;
private final Set<ImageAnnotation> annotations = new HashSet<>();
private ImageResolver imageResolver;
private boolean computingChanges;
private final AnnotationPainter painter;
private boolean armed = false;
private Cursor cursor;
private Cursor previousCursor;
public ImageManager(HtmlViewer viewer, ImageCache imageCache, AnnotationPainter painter) {
this.viewer = viewer;
this.painter = painter;
display = viewer.getTextWidget().getDisplay();
this.imageCache = imageCache;
inspect();
viewer.getTextWidget().addDisposeListener(this);
viewer.addTextInputListener(this);
if (viewer.getDocument() != null) {
viewer.getDocument().addDocumentListener(this);
}
viewer.addSelectionChangedListener(this);
viewer.addPostSelectionChangedListener(this);
// bug 257868 support image hyperlinks
HyperlinkMouseListener mouseListener = new HyperlinkMouseListener();
viewer.getTextWidget().addMouseMoveListener(mouseListener);
viewer.getTextWidget().addMouseListener(mouseListener);
}
private IHyperlink getHyperlink(MouseEvent e) {
// bug 257868 support image hyperlinks
if (annotations.isEmpty()) {
return null;
}
Point point = new Point(e.x, e.y);
for (ImageAnnotation annotation : annotations) {
if (annotation.getHyperlnkAnnotation() == null) {
continue;
}
Rectangle region = getRegion(annotation);
if (region != null) {
if (region.contains(point)) {
AnnotationHyperlinkDetector detector = (AnnotationHyperlinkDetector) viewer.getTextWidget()
.getData(AnnotationHyperlinkDetector.class.getName());
if (detector != null) {
IHyperlink hyperlink = detector.createHyperlink(viewer, viewer.getAnnotationModel(),
annotation.getHyperlnkAnnotation());
return hyperlink;
}
}
}
}
return null;
}
public void clicked(MouseEvent e) {
IHyperlink hyperlink = getHyperlink(e);
if (hyperlink != null) {
disarm();
hyperlink.open();
}
}
/**
* get the widget-relative region of the given annotation
*
* @return the region, or null if it is unknown
*/
private Rectangle getRegion(ImageAnnotation annotation) {
if (annotation.getImage() == null) {
return null;
}
Position position = viewer.getAnnotationModel().getPosition(annotation);
Point locationAtOffset = viewer.getTextWidget().getLocationAtOffset(position.offset);
Rectangle bounds = annotation.getImage().getBounds();
Rectangle rectange = new Rectangle(locationAtOffset.x, locationAtOffset.y, bounds.width, bounds.height);
return rectange;
}
void adjust(MouseEvent e) {
IHyperlink hyperlink = getHyperlink(e);
if (hyperlink == null) {
disarm();
} else {
// always arm here even if already armed, otherwise the cursor
// setting interacts poorly with other things that set the cursor.
armed = true;
Cursor currentCursor = viewer.getTextWidget().getCursor();
if (cursor == null || currentCursor != cursor) {
previousCursor = currentCursor;
if (cursor == null) {
cursor = new Cursor(viewer.getTextWidget().getDisplay(), SWT.CURSOR_HAND);
}
viewer.getTextWidget().setCursor(cursor);
}
}
}
void disarm() {
if (armed) {
if (previousCursor != null) {
viewer.getTextWidget().setCursor(previousCursor);
}
armed = false;
}
}
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
stop();
}
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
if (oldInput != null) {
oldInput.removeDocumentListener(this);
}
if (newInput != null) {
newInput.addDocumentListener(this);
}
inspect();
}
@SuppressWarnings("unchecked")
private void inspect() {
synchronized (this) {
annotations.clear();
if (viewer.getAnnotationModel() != null) {
Iterator<Annotation> iterator = viewer.getAnnotationModel().getAnnotationIterator();
while (iterator.hasNext()) {
Annotation annotation = iterator.next();
if (annotation instanceof ImageAnnotation) {
annotations.add((ImageAnnotation) annotation);
}
}
}
}
if (!annotations.isEmpty()) {
ImageResolver resolver;
synchronized (this) {
resolver = imageResolver;
}
if (resolver != null) {
try {
resolver.join();
} catch (InterruptedException e) {
return;
}
}
imageResolver = new ImageResolver();
imageResolver.start();
}
}
private synchronized void stop() {
ImageResolver resolver = imageResolver;
if (resolver != null) {
resolver.interrupt();
}
}
public void widgetDisposed(DisposeEvent e) {
if (cursor != null) {
cursor.dispose();
cursor = null;
}
stop();
}
@SuppressWarnings("unchecked")
private void updateImage(String imgSrc, ImageData imageData) {
if (display.isDisposed() || viewer.getTextWidget().isDisposed()) {
return;
}
Image image = imageData == null
? imageCache.getMissingImage()
: ImageDescriptor.createFromImageData(imageData).createImage();
imageCache.putImage(imgSrc, image);
Set<ImageAnnotation> modifiedAnnotations = new HashSet<>();
AnnotationModel annotationModel = (AnnotationModel) viewer.getAnnotationModel();
Object annotationLockObject = annotationModel.getLockObject();
if (annotationLockObject == null) {
annotationLockObject = annotationModel;
}
synchronized (annotationLockObject) {
Iterator<Annotation> iterator = annotationModel.getAnnotationIterator();
while (iterator.hasNext()) {
Annotation annotation = iterator.next();
if (annotation instanceof ImageAnnotation) {
ImageAnnotation imageAnnotation = (ImageAnnotation) annotation;
if (imgSrc.equals(imageAnnotation.getUrl())) {
imageAnnotation.setImage(image);
modifiedAnnotations.add(imageAnnotation);
}
}
}
}
if (!modifiedAnnotations.isEmpty()) {
computingChanges = true;
try {
boolean rangesAdjusted = false;
List<StyleRange> ranges = new ArrayList<>();
Iterator<?> allStyleRangeIterator = viewer.getTextPresentation().getAllStyleRangeIterator();
while (allStyleRangeIterator.hasNext()) {
StyleRange range = (StyleRange) allStyleRangeIterator.next();
ranges.add((StyleRange) range.clone());
}
GC gc = new GC(viewer.getTextWidget());
try {
viewer.getTextWidget().setRedraw(false);
TextPresentation textPresentation = viewer.getTextPresentation();
// textPresentation.
for (ImageAnnotation annotation : modifiedAnnotations) {
int height = annotation.getImage().getBounds().height;
Position position = annotationModel.getPosition(annotation);
String widgetText = viewer.getTextWidget().getText();
Font font = null;
if (widgetText.length() > 0 && widgetText.length() > position.offset) {
StyleRange styleRange = viewer.getTextWidget().getStyleRangeAtOffset(position.offset);
if (styleRange != null) {
font = styleRange.font;
}
}
if (font == null) {
font = viewer.getTextWidget().getFont();
}
gc.setFont(font);
Point extent = gc.textExtent("\n"); //$NON-NLS-1$
if (extent.y > 0) {
int numNewlines = (int) Math.ceil(((double) height) / ((double) extent.y));
final int originalNewlines = numNewlines;
IDocument document = viewer.getDocument();
try {
for (int x = position.offset; x < document.getLength(); ++x) {
if (document.getChar(x) == '\n') {
if (x != position.offset
&& Util.annotationsIncludeOffset(viewer.getAnnotationModel(), x)) {
break;
}
--numNewlines;
} else {
break;
}
}
if (numNewlines > 0) {
String newlines = ""; //$NON-NLS-1$
for (int x = 0; x < numNewlines; ++x) {
newlines += "\n"; //$NON-NLS-1$
}
document.replace(position.offset + 1, 0, newlines);
} else if (numNewlines < 0) {
document.replace(position.offset, -numNewlines, ""); //$NON-NLS-1$
}
if (numNewlines != 0) {
// no need to fixup other annotation positions, since the annotation model is hooked into the document.
// fix up styles
for (StyleRange range : ranges) {
if (range.start > position.offset) {
range.start += numNewlines;
rangesAdjusted = true;
} else if (range.start + range.length > position.offset) {
range.length += numNewlines;
rangesAdjusted = true;
}
}
}
// bug# 248643: update the annotation size to reflect the full size of the image
// so that it gets repainted when some portion of the image is exposed
// as a result of scrolling
if (position.getLength() != originalNewlines) {
annotationModel.modifyAnnotationPosition(annotation,
new Position(position.offset, originalNewlines));
}
} catch (BadLocationException e) {
// ignore
}
}
}
if (rangesAdjusted) {
TextPresentation presentation = new TextPresentation();
if (textPresentation.getDefaultStyleRange() != null) {
StyleRange defaultStyleRange = (StyleRange) textPresentation.getDefaultStyleRange().clone();
if (viewer.getDocument() != null) {
if (defaultStyleRange.length < viewer.getDocument().getLength()) {
defaultStyleRange.length = viewer.getDocument().getLength();
}
}
presentation.setDefaultStyleRange(defaultStyleRange);
}
for (StyleRange range : ranges) {
presentation.addStyleRange(range);
}
viewer.setTextPresentation(presentation);
viewer.invalidateTextPresentation();
}
} finally {
viewer.getTextWidget().setRedraw(true);
gc.dispose();
}
viewer.getTextWidget().redraw();
} finally {
computingChanges = false;
}
}
}
private static final AtomicInteger resolverIdSeed = new AtomicInteger(1);
private class ImageResolver extends Thread {
public ImageResolver() {
setName(ImageResolver.class.getSimpleName() + '-' + resolverIdSeed.getAndIncrement());
setDaemon(true);
}
@Override
public void run() {
try {
final Map<String, ImageData> urlToImageData = new HashMap<>();
for (ImageAnnotation annotation : annotations) {
final String imgSrc = annotation.getUrl();
if (imgSrc != null && !urlToImageData.containsKey(imgSrc)) {
try {
URL location = imageCache.getBase() == null
? new URL(imgSrc)
: new URL(imageCache.getBase(), imgSrc);
try {
InputStream in = new BufferedInputStream(location.openStream());
try {
urlToImageData.put(imgSrc, new ImageData(in));
} catch (SWTException e) {
if (e.code != SWT.ERROR_INVALID_IMAGE) {
throw e;
}
urlToImageData.put(imgSrc, null);
} finally {
in.close();
}
} catch (Exception e) {
if (WikiTextUiPlugin.getDefault() != null) {
WikiTextUiPlugin.getDefault().log(IStatus.ERROR,
NLS.bind(Messages.ImageManager_accessFailed, new Object[] { location }), e);
}
urlToImageData.put(imgSrc, null);
}
} catch (MalformedURLException e) {
urlToImageData.put(imgSrc, null);
}
display.asyncExec(new Runnable() {
public void run() {
updateImage(imgSrc, urlToImageData.get(imgSrc));
}
});
}
if (Thread.currentThread().isInterrupted()) {
break;
}
}
} finally {
imageResolver = null;
}
}
}
public void documentAboutToBeChanged(DocumentEvent event) {
if (computingChanges) {
return;
}
stop();
}
public void documentChanged(DocumentEvent event) {
if (computingChanges) {
return;
}
inspect();
}
public void selectionChanged(SelectionChangedEvent event) {
GC gc = new GC(viewer.getTextWidget());
try {
Event e = new Event();
e.gc = gc;
e.widget = viewer.getTextWidget();
Rectangle bounds = viewer.getTextWidget().getBounds();
e.height = bounds.height;
e.width = bounds.width;
e.x = 0;
e.y = 0;
PaintEvent paintEvent = new PaintEvent(e);
painter.paintControl(paintEvent);
} finally {
gc.dispose();
}
}
}