| /******************************************************************************* |
| * 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(); |
| } |
| } |
| |
| } |