/******************************************************************************* * Copyright (c) 2000, 2010 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.draw2d.parts; import java.util.Iterator; import java.util.Map; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.SWTGraphics; import org.eclipse.draw2d.ScaledGraphics; import org.eclipse.draw2d.UpdateListener; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; /** * A Thumbnail is a Figure that displays an image of its source Figure at a * smaller size. The Thumbnail will maintain the aspect ratio of the source * Figure. * * @author Eric Bordeau * @author Alexander Nyßen (anyssen) */ public class Thumbnail extends Figure implements UpdateListener { /** * This updates the Thumbnail by breaking the thumbnail {@link Image} into * several tiles and updating each tile individually. */ class ThumbnailUpdater implements Runnable { private static final int MIN_TILE_SIZE = 256; private static final int MAX_NUMBER_OF_TILES = 16; private int currentHTile, currentVTile; private int hTiles, vTiles; private Dimension tileSize; private Dimension sourceSize; // the source size that was used for the // tileSize computation private boolean isActive = true; private boolean isRunning = false; private Image tileImage; private Dimension tileImageSize; // GC and Graphics to let the source figure paint on the tile image private GC tileGC; private SWTGraphics tileGCGraphics; private ScaledGraphics tileGraphics; // GC used to copy from the tile image into the thumbnail image private GC thumbnailGC; /** * Stops the updater and disposes of any resources. */ public void deactivate() { setActive(false); stop(); if (thumbnailImage != null) { thumbnailImage.dispose(); thumbnailImage = null; thumbnailImageSize = null; } } /** * Returns the current horizontal tile index. * * @return current horizontal tile index. */ protected int getCurrentHTile() { return currentHTile; } /** * Returns the current vertical tile index. * * @return current vertical tile index. */ protected int getCurrentVTile() { return currentVTile; } /** * Returns <code>true</code> if this ThumbnailUpdater is active. An * inactive updater has disposed of its {@link Image}. The updater may * be active and not currently running. * * @return <code>true</code> if this ThumbnailUpdater is active */ public boolean isActive() { return isActive; } /** * Returns <code>true</code> if this is currently running and updating * at least one tile on the thumbnail {@link Image}. * * @return <code>true</code> if this is currently running */ public boolean isRunning() { return isRunning; } /** * Resets the number of vertical and horizontal tiles, as well as the * tile size and current tile index. */ public void resetTileValues() { // Keep track of source size that matches the computed tile size. sourceSize = getSourceRectangle().getSize(); // Compute number of horizontal and vertical tiles and the size of // each tile (while the last tile in horizontal and vertical // direction may be smaller); ensure that all tiles except those on // the bottom and right border will have at least a size of // MIN_TILE_SIZE size and that at most MAX_NUMBER_OF_TILES tiles // will be created. hTiles = Math.min( (int) Math.ceil((float) sourceSize.width / (float) MIN_TILE_SIZE), MAX_NUMBER_OF_TILES); vTiles = Math.min( (int) Math.ceil((float) sourceSize.height / (float) MIN_TILE_SIZE), MAX_NUMBER_OF_TILES); tileSize = new Dimension((int) Math.ceil((float) sourceSize.width / (float) hTiles), (int) Math.ceil((float) sourceSize.height / (float) vTiles)); // Reset the current indices so that the next update will start with // the first tile in horizontal and vertical direction currentHTile = 0; currentVTile = 0; } /** * Restarts the updater. */ public void restart() { stop(); start(); } /** * Updates the current tile on the Thumbnail. An area of the source * Figure is painted to an {@link Image}. That Image is then drawn on * the Thumbnail. Scaling of the source Image is done inside * {@link GC#drawImage(Image, int, int, int, int, int, int, int, int)} * since the source and target sizes are different. The current tile * indexes are incremented and if more updating is necesary, this * {@link Runnable} is called again in a * {@link Display#timerExec(int, Runnable)}. If no more updating is * required, {@link #stop()} is called. */ public void run() { if (!isActive() || !isRunning() || tileGraphics == null) return; int v = getCurrentVTile(); int sy1 = v * tileSize.height; int sy2 = Math.min((v + 1) * tileSize.height, sourceSize.height); int h = getCurrentHTile(); int sx1 = h * tileSize.width; int sx2 = Math.min((h + 1) * tileSize.width, sourceSize.width); tileGraphics.pushState(); // clear the background (by filling with the background color) Rectangle rect = new Rectangle(0, 0, sx2 - sx1, sy2 - sy1); tileGraphics.fillRectangle(rect); // Let the source figure paint into the tile image. // IMPORTANT (fix for bug #309912): we do not let the source figure // paint directly into the thumbnail image, because we cannot ensure // that it paints completely inside the current tile area (it may // set its own clip inside paint(Graphics) and overwrite areas of // tiles that have already been rendered. By providing an own tile // image and copying from it into the thumbnail image, we are safe. org.eclipse.draw2d.geometry.Point p = getSourceRectangle() .getLocation(); tileGraphics.translate(-p.x * getScaleX() - sx1, -p.y * getScaleY() - sy1); tileGraphics.scale(getScaleX()); sourceFigure.paint(tileGraphics); tileGraphics.popState(); // Copy the painted tile image into the thumbnail image. thumbnailGC.drawImage(tileImage, 0, 0, sx2 - sx1, sy2 - sy1, sx1, sy1, sx2 - sx1, sy2 - sy1); if (getCurrentHTile() < (hTiles - 1)) setCurrentHTile(getCurrentHTile() + 1); else { setCurrentHTile(0); if (getCurrentVTile() < (vTiles - 1)) setCurrentVTile(getCurrentVTile() + 1); else setCurrentVTile(0); } if (getCurrentHTile() != 0 || getCurrentVTile() != 0) { Display.getCurrent().asyncExec(this); } else if (isDirty()) { setDirty(false); Display.getCurrent().asyncExec(this); repaint(); } else { stop(); repaint(); } } /** * Sets the active flag. * * @param value * The active value */ public void setActive(boolean value) { isActive = value; } /** * Sets the current horizontal tile index. * * @param count * current horizontal tile index */ protected void setCurrentHTile(int count) { currentHTile = count; } /** * Sets the current vertical tile index. * * @param count * current vertical tile index */ protected void setCurrentVTile(int count) { currentVTile = count; } /** * Starts this updater. This method initializes all the necessary * resources and puts this {@link Runnable} on the asynch queue. If this * updater is not active or is already running, this method just * returns. */ public void start() { if (!isActive() || isRunning()) return; isRunning = true; setDirty(false); resetTileValues(); if (!targetSize.equals(thumbnailImageSize)) { resetThumbnailImage(); } if (targetSize.isEmpty()) return; // UNSUPPORTED - image constructor not implemented in RAP // thumbnailGC = new GC(thumbnailImage, SWT.NONE); thumbnailGC = new GC(thumbnailImage.getDevice(),SWT.NONE); if (!tileSize.equals(tileImageSize)) { resetTileImage(); } //tileGC = new GC(tileImage, tileGC = new GC(tileImage.getDevice(), sourceFigure.isMirrored() ? SWT.RIGHT_TO_LEFT : SWT.NONE); tileGCGraphics = new SWTGraphics(tileGC); tileGraphics = new ScaledGraphics(tileGCGraphics); Color color = sourceFigure.getForegroundColor(); if (color != null) tileGraphics.setForegroundColor(color); color = sourceFigure.getBackgroundColor(); if (color != null) tileGraphics.setBackgroundColor(color); tileGraphics.setFont(sourceFigure.getFont()); setScales(targetSize.width / (float) sourceSize.width, targetSize.height / (float) sourceSize.height); Display.getCurrent().asyncExec(this); } private void resetThumbnailImage() { if (thumbnailImage != null) thumbnailImage.dispose(); if (!targetSize.isEmpty()) { thumbnailImage = new Image(Display.getDefault(), targetSize.width, targetSize.height); thumbnailImageSize = new Dimension(targetSize); } else { thumbnailImage = null; thumbnailImageSize = new Dimension(0, 0); } } private void resetTileImage() { if (tileImage != null) tileImage.dispose(); if (!tileSize.isEmpty()) { tileImage = new Image(Display.getDefault(), tileSize.width, tileSize.height); tileImageSize = new Dimension(tileSize); } else { tileImage = null; tileImageSize = new Dimension(0, 0); } } /** * Stops this updater. Also disposes of resources (except the thumbnail * image which is still needed for painting). */ public void stop() { isRunning = false; if (tileGraphics != null) { tileGraphics.dispose(); tileGraphics = null; } if (tileGCGraphics != null) { tileGCGraphics.dispose(); tileGCGraphics = null; } if (tileGC != null) { tileGC.dispose(); tileGC = null; } if (thumbnailGC != null) { thumbnailGC.dispose(); thumbnailGC = null; } if (tileImage != null) { tileImage.dispose(); tileImage = null; tileImageSize = null; } // Don't dispose of the thumbnail image since it is needed to paint // the figure when the source is not dirty (i.e. showing/hiding the // dock). } } private boolean isDirty; private float scaleX; private float scaleY; private IFigure sourceFigure; Dimension targetSize = new Dimension(0, 0); private Image thumbnailImage; private Dimension thumbnailImageSize; private ThumbnailUpdater updater = new ThumbnailUpdater(); /** * Creates a new Thumbnail. The source Figure must be set separately if you * use this constructor. */ public Thumbnail() { super(); } /** * Creates a new Thumbnail with the given IFigure as its source figure. * * @param fig * The source figure */ public Thumbnail(IFigure fig) { this(); setSource(fig); } private Dimension adjustToAspectRatio(Dimension size, boolean adjustToMaxDimension) { Dimension sourceSize = getSourceRectangle().getSize(); Dimension borderSize = new Dimension(getInsets().getWidth(), getInsets().getHeight()); size.expand(borderSize.getNegated()); int width, height; if (adjustToMaxDimension) { width = Math.max(size.width, (int) (size.height * sourceSize.width / (float) sourceSize.height + 0.5)); height = Math.max(size.height, (int) (size.width * sourceSize.height / (float) sourceSize.width + 0.5)); } else { width = Math.min(size.width, (int) (size.height * sourceSize.width / (float) sourceSize.height + 0.5)); height = Math.min(size.height, (int) (size.width * sourceSize.height / (float) sourceSize.width + 0.5)); } size.width = width; size.height = height; return size.expand(borderSize); } /** * Deactivates this Thumbnail. */ public void deactivate() { sourceFigure.getUpdateManager().removeUpdateListener(this); updater.deactivate(); } /** * Returns the preferred size of this Thumbnail. The preferred size will be * calculated in a way that maintains the source Figure's aspect ratio. * * @param wHint * The width hint * @param hHint * The height hint * @return The preferred size */ public Dimension getPreferredSize(int wHint, int hHint) { if (prefSize == null) return adjustToAspectRatio(getBounds().getSize(), false); Dimension preferredSize = adjustToAspectRatio(prefSize.getCopy(), true); if (maxSize == null) return preferredSize; Dimension maximumSize = adjustToAspectRatio(maxSize.getCopy(), true); if (preferredSize.contains(maximumSize)) return maximumSize; else return preferredSize; } /** * Returns the scale factor on the X-axis. * * @return X scale */ protected float getScaleX() { return scaleX; } /** * Returns the scale factor on the Y-axis. * * @return Y scale */ protected float getScaleY() { return scaleY; } /** * Returns the source figure being used to generate a thumbnail. * * @return the source figure */ protected IFigure getSource() { return sourceFigure; } /** * Returns the rectangular region relative to the source figure which will * be the basis of the thumbnail. The value may be returned by reference and * should not be modified by the caller. * * @since 3.1 * @return the region of the source figure being used for the thumbnail */ protected Rectangle getSourceRectangle() { return sourceFigure.getBounds(); } /** * Returns the scaled Image of the source Figure. If the Image needs to be * updated, the ThumbnailUpdater will notified. * * @return The thumbnail image */ protected Image getThumbnailImage() { Dimension oldSize = targetSize; targetSize = getPreferredSize(); targetSize.expand(new Dimension(getInsets().getWidth(), getInsets() .getHeight()).negate()); setScales(targetSize.width / (float) getSourceRectangle().width, targetSize.height / (float) getSourceRectangle().height); if ((isDirty()) && !updater.isRunning()) updater.start(); else if (oldSize != null && !targetSize.equals(oldSize)) { revalidate(); updater.restart(); } return thumbnailImage; } /** * Returns <code>true</code> if the source figure has changed. * * @return <code>true</code> if the source figure has changed */ protected boolean isDirty() { return isDirty; } /** * @see org.eclipse.draw2d.UpdateListener#notifyPainting(Rectangle, Map) */ public void notifyPainting(Rectangle damage, Map dirtyRegions) { Iterator dirtyFigures = dirtyRegions.keySet().iterator(); while (dirtyFigures.hasNext()) { IFigure current = (IFigure) dirtyFigures.next(); while (current != null) { if (current == getSource()) { setDirty(true); repaint(); return; } current = current.getParent(); } } } /** * @see org.eclipse.draw2d.UpdateListener#notifyValidating() */ public void notifyValidating() { // setDirty(true); // revalidate(); } /** * @see org.eclipse.draw2d.Figure#paintFigure(Graphics) */ protected void paintFigure(Graphics graphics) { Image thumbnail = getThumbnailImage(); if (thumbnail == null) return; graphics.drawImage(thumbnail, getClientArea().getLocation()); } /** * Sets the dirty flag. * * @param value * The dirty value */ public void setDirty(boolean value) { isDirty = value; } /** * Sets the X and Y scales for the Thumbnail. These scales represent the * ratio between the source figure and the Thumbnail. * * @param x * The X scale * @param y * The Y scale */ protected void setScales(float x, float y) { scaleX = x; scaleY = y; } /** * Sets the source Figure. Also sets the scales and creates the necessary * update manager. * * @param fig * The source figure */ public void setSource(IFigure fig) { if (sourceFigure == fig) return; if (sourceFigure != null) sourceFigure.getUpdateManager().removeUpdateListener(this); sourceFigure = fig; if (sourceFigure != null) { setScales((float) getSize().width / (float) getSourceRectangle().width, (float) getSize().height / (float) getSourceRectangle().height); sourceFigure.getUpdateManager().addUpdateListener(this); repaint(); } } }