/**

 TrakEM2 plugin for ImageJ(C).
 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
 **/

package ini.trakem2.display;

import java.awt.AWTException;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Robot;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.scijava.vecmath.Point2f;
import org.scijava.vecmath.Vector2f;
import org.scijava.vecmath.Vector3d;

import ij.IJ;
import ij.ImagePlus;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.ImageCanvas;
import ij.gui.Roi;
import ij.gui.Toolbar;
import ij.measure.Calibration;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ini.trakem2.Project;
import ini.trakem2.display.graphics.GraphicsSource;
import ini.trakem2.display.inspect.InspectPatchTrianglesMode;
import ini.trakem2.imaging.Segmentation;
import ini.trakem2.persistence.Loader;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Lock;
import ini.trakem2.utils.ProjectToolbar;
import ini.trakem2.utils.Search;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;

public final class DisplayCanvas extends ImageCanvas implements KeyListener/*, FocusListener*/, MouseWheelListener {

	private static final long serialVersionUID = 1L;

	private Display display;

	private boolean update_graphics = false;
	private BufferedImage offscreen = null;
	private final HashSet<BufferedImage> to_flush = new HashSet<BufferedImage>();
	private ArrayList<Displayable> al_top = new ArrayList<Displayable>();

	private final Lock lock_paint = new Lock();

	private Rectangle box = null; // the bounding box of the active

	private FakeImageWindow fake_win;

	private FreeHandProfile freehandProfile = null;
	private Robot r;// used for setting the mouse pointer

	private final Object offscreen_lock = new Object();

	private Cursor noCursor;

	private boolean snapping = false;
	private boolean dragging = false;
	private boolean input_disabled = false;
	private boolean input_disabled2 = false;

	/** Store a copy of whatever data as each Class may define it, one such data object per class.
	 * Private to the package. */
	static private Hashtable<Class<?>,Object> copy_buffer = new Hashtable<Class<?>,Object>();

	static void setCopyBuffer(final Class<?> c, final Object ob) { copy_buffer.put(c, ob); }
	static Object getCopyBuffer(final Class<?> c) { return copy_buffer.get(c); }

	static private boolean openglEnabled = false;
	static private boolean quartzEnabled = false;
	static private boolean ddscaleEnabled = false;

	// Private to the display package:
	static final RenderingHints rhints;

	/** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent; */
	static {
		final Map<RenderingHints.Key, Object> hints = new HashMap<RenderingHints.Key, Object>();
		try {
			final String openglProperty = System.getProperty("sun.java2d.opengl");
			openglEnabled = openglProperty != null && Boolean.parseBoolean(openglProperty);
		} catch (final Exception ex) { }
		try {
			final String quartzProperty = System.getProperty("apple.awt.graphics.UseQuartz");
			quartzEnabled = Boolean.parseBoolean(quartzProperty);
		} catch (final Exception ex) { }
		try {
			final String ddscaleProperty = System.getProperty("sun.java2d.ddscale");
			final String d3dProperty = System.getProperty("sun.java2d.d3d");
			ddscaleEnabled = Boolean.parseBoolean(ddscaleProperty) && Boolean.parseBoolean(d3dProperty);
		} catch (final Exception ex) { }

		if (openglEnabled) {
			// Bilinear interpolation can be accelerated by the OpenGL pipeline
			hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
			hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

		} else if (quartzEnabled) {
			//hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
			hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
			hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
			hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

		} else if (ddscaleEnabled) {
			hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		}

		rhints = new RenderingHints(hints);
	}

	private VolatileImage volatileImage;
	private Object volatile_lock = new Object();
	//private javax.swing.Timer resourceTimer = new javax.swing.Timer(10000, resourceReaper);
	//private boolean frameRendered = false;
	private boolean invalid_volatile = false;

	/** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent.
	 *  MUST be called within a "synchronized (volatile_lock) { ... }" block. */
	private void renderVolatileImage(final GraphicsConfiguration gc, final BufferedImage offscreen,
			final ArrayList<Displayable> top, final Displayable active,
			final Layer active_layer, final List<Layer> layers,
			final int c_alphas, final AffineTransform at, Rectangle clipRect) {
		do {
			// Recreate volatileImage ONLY if necessary: when null, when incompatible, or when dimensions have changed
			// Otherwise, just paint on top of it
			final int w = getWidth(), h = getHeight();
			if (0 == w || 0 == h) return;
			if (null == volatileImage || volatileImage.getWidth() != w
			  || volatileImage.getHeight() != h || volatileImage.validate(gc) == VolatileImage.IMAGE_INCOMPATIBLE) {
				if (null != volatileImage) volatileImage.flush();
				volatileImage = gc.createCompatibleVolatileImage(w, h);
				volatileImage.setAccelerationPriority(1.0f);
				invalid_volatile = false;
				clipRect = null; // paint all
			}
			//
			// Now paint the BufferedImage into the accelerated image
			//
			final Graphics2D g = volatileImage.createGraphics();

			// 0 - set clipRect
			if (null != clipRect) g.setClip(clipRect);

			// 1 - Erase any background
			g.setColor(Color.black);
			if (null == clipRect) g.fillRect(0, 0, w, h);
			else g.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);

			// 2 - Paint offscreen image
			if (null != offscreen) g.drawImage(offscreen, 0, 0, null);

			// 3 - Paint the active Displayable and all cached on top

			//Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
			//Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			//Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
			g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

			if (null != active_layer) {
				g.setTransform(at);
				g.setStroke(this.stroke); // AFTER setting the transform
				// Active has to be painted wherever it is, within al_top, if it's not an image
				//if (null != active && active.getClass() != Patch.class && !active.isOutOfRepaintingClip(magnification, srcRect, clipRect)) active.paint(g, magnification, true, c_alphas, active_layer);
				final boolean must_paint_active = null != active && active.isVisible() && !ImageData.class.isAssignableFrom(active.getClass());
				boolean active_painted = !must_paint_active;

				if (null != top) {
					final Rectangle tmp = null != clipRect ? new Rectangle() : null;
					final Rectangle clip = null != clipRect ? new Rectangle((int)(clipRect.x * magnification) - srcRect.x, (int)(clipRect.y * magnification) - srcRect.y, (int)(clipRect.width * magnification), (int)(clipRect.height * magnification)) : null;
					for (final Displayable d : top) {
						if (null != clipRect && !d.getBoundingBox(tmp).intersects(clip)) continue;
						d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
						if (active_painted) continue;
						else active_painted = d == active;
					}
				}
				if (must_paint_active && !active_painted) {
					// Active may not have been part of top array if it was added new and the offscreen image was not updated,
					// which is the case for any non-image object
					// Or, when selecting an object if there were none selected yet.
					active.paint(g, srcRect, magnification, true, c_alphas, active_layer, layers);
				}
			}

			display.getMode().getGraphicsSource().paintOnTop(g, display, srcRect, magnification);

			if (null != active_layer.getOverlay2())
				active_layer.getOverlay2().paint(g, srcRect, magnification);
			if (null != active_layer.getParent().getOverlay2())
				active_layer.getParent().getOverlay2().paint(g, srcRect, magnification);

			if (null != display.gridoverlay) display.gridoverlay.paint(g);

			/* // debug: paint the ZDisplayable's bucket in this layer
			if (null != active_layer.getParent().lbucks) {
				active_layer.getParent().lbucks.get(active_layer).root.paint(g, srcRect, magnification, Color.red);
			}
			*/

			g.dispose();
		} while (volatileImage.contentsLost());
	}

	/** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent;
	 *  Paints (and re-renders, if necessary) the volatile image onto the given Graphics object, which
	 *  is that of the DisplayCanvas as provided to the paint(Graphics g) method.
	 *
	 *  Expects clipRect in screen coordinates
	 */
	private void render(final Graphics g, final Displayable active, final Layer active_layer,
			final List<Layer> layers, final int c_alphas, final AffineTransform at, Rectangle clipRect) {
		final Graphics2D g2d = (Graphics2D) g.create();
		g2d.setRenderingHints(rhints);
		do {
			final ArrayList<Displayable> top;
			final BufferedImage offscreen;
			synchronized (offscreen_lock) {
				offscreen = this.offscreen;
				top = this.al_top; // will never be cleared, but may be swapped
			}
			final GraphicsConfiguration gc = getGraphicsConfiguration();
			display.getProject().getLoader().releaseToFit(getWidth() * getHeight() * 4 * 5); // 5 images

			// Protect volatile image while rendering it
			synchronized (volatile_lock) {
				if (invalid_volatile || null == volatileImage
				 || volatileImage.validate(gc) != VolatileImage.IMAGE_OK)
				{
					// clear clip, remade in full
					clipRect = null;
					renderVolatileImage(gc, offscreen, top, active, active_layer, layers, c_alphas, at, clipRect);
				}
				if (null != clipRect) g2d.setClip(clipRect);
				g2d.drawImage(volatileImage, 0, 0, null);
			}
		} while (volatileImage.contentsLost());

		g2d.dispose();

		// Flush all old offscreen images
		synchronized (offscreen_lock) {
			for (final BufferedImage bi : to_flush) {
				bi.flush();
			}
			to_flush.clear();
		}
	}

	protected void invalidateVolatile() {
		synchronized (volatile_lock) {
			this.invalid_volatile = true;
		}
	}

	/////////////////

	public DisplayCanvas(final Display display, final int width, final int height) {
		super(new FakeImagePlus(width, height, display));
		fake_win = new FakeImageWindow(imp, this, display);
		this.display = display;
		this.imageWidth = width;
		this.imageHeight = height;
		removeKeyListener(IJ.getInstance());
		addKeyListener(this);
		addMouseWheelListener(this);
	}

	public Display getDisplay() { return display; }

	/** Used to constrain magnification so that only snapshots are used for painting when opening a new, large and filled Display. */
	protected void setInitialMagnification(final double mag) { // calling this method 'setMagnification' would conflict with the super class homonimous method.
		this.magnification = mag; // don't save in the database. This value is overriden when reopening from the database by calling the setup method.
	}

	/** Used for restoring properties from the database. */
	public void setup(final double mag, final Rectangle srcRect) {
		this.magnification = mag;
		this.srcRect = (Rectangle)srcRect.clone(); // just in case
		super.setDrawingSize((int)Math.ceil(srcRect.width * mag), (int)Math.ceil(srcRect.height * mag));
		setMagnification(mag);
		//no longer needed//display.pack(); // TODO should be run via invokeLater ... need to check many potential locks of invokeLater calling each other.
	}

	/** Does not repaint. */
	public void setDimensions(final double width, final double height) {
		this.imageWidth = (int)Math.ceil(width);
		this.imageHeight = (int)Math.ceil(height);
		((FakeImagePlus)imp).setDimensions(imageWidth, imageHeight);
	}

	/** Overriding to disable it. */
	public void handlePopupMenu() {}

	@Override
	public final void update(final Graphics g) {
		// overriding to avoid default behaviour in java.awt.Canvas which consists in first repainting the entire drawable area with the background color, and then calling method paint.
		this.paint(g);
	}

	/** Handles repaint event requests and the generation of offscreen threads. */
	private final AbstractRepaintThread RT = new AbstractRepaintThread(this, "T2-Canvas-Repainter", new OffscreenThread()) {
		@Override
		protected void handleUpdateGraphics(final Component target, final Rectangle clipRect) {
			final Layer active_layer = display.getLayer();
			this.off.setProperties(new RepaintProperties(clipRect, active_layer, active_layer.getParent().getColorCueLayerRange(active_layer), target.getWidth(), target.getHeight(), srcRect, magnification, display.getActive(), display.getDisplayChannelAlphas(), display.getMode().getGraphicsSource()));
		}
	};

	/*
	private final void setRenderingHints(final Graphics2D g) {
		// so slow!! Particularly the first one.
		g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
		//g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		//g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
		//g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		//g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
	}
	*/

	@Override
	public void setMagnification(double mag) {
		if (mag < 0.00000001) mag = 0.00000001;
		// ensure a stroke of thickness 1.0 regardless of magnification
		this.stroke = new BasicStroke((float)(1.0/mag), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
		// FIXES MAG TO ImageCanvas.zoomLevel LIMITS!!
		//super.setMagnification(mag);
		// So, manually:
		this.magnification = mag;
		imp.setTitle(imp.getTitle());
		display.getMode().magnificationUpdated(srcRect, mag);
	}

	/** Paint lines always with a thickness of 1 pixel. This stroke is modified when the magnification is changed, to compensate. */
	private BasicStroke stroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);

	/** The affine transform representing the srcRect displacement and the magnification. */
	private final AffineTransform atc = new AffineTransform();

	@Override
	public void paint(final Graphics g) {
		if (null == g) return;
		try {
			synchronized (lock_paint) {
				lock_paint.lock();
			}

			// ensure proper positioning
			g.translate(0, 0); // ints!

			final Rectangle clipRect = g.getClipBounds();

			final Displayable active = display.getActive();
			final int c_alphas = display.getDisplayChannelAlphas();

			final Layer active_layer = display.getLayer();
			final List<Layer> layers = active_layer.getParent().getColorCueLayerRange(active_layer);

			final Graphics2D g2d = (Graphics2D)g;

			// prepare the canvas for the srcRect and magnification
			final AffineTransform at_original = g2d.getTransform();
			atc.setToIdentity();
			atc.scale(magnification, magnification);
			atc.translate(-srcRect.x, -srcRect.y);
			at_original.preConcatenate(atc);

			if (null != offscreen && dragging) invalidateVolatile(); // to update the active at least
			render(g, active, active_layer, layers, c_alphas, at_original, clipRect);

			g2d.setTransform(at_original);

			g2d.setStroke(this.stroke);

			// debug buckets
			//if (null != display.getLayer().root) display.getLayer().root.paint(g2d, srcRect, magnification, Color.red);
			//if (null != display.getLayer().getParent().lbucks.get(display.getLayer()).root) display.getLayer().getParent().lbucks.get(display.getLayer()).root.paint(g2d, srcRect, magnification, Color.blue);


			// reset to identity
			g2d.setTransform(new AffineTransform());
			// reset to 1.0 thickness
			g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));

			// paint brush outline for AreaList, or fast-marching area
			if (mouse_in && null != active && AreaContainer.class.isInstance(active)) {
				switch (ProjectToolbar.getToolId()) {
					case ProjectToolbar.BRUSH:
						final int brushSize = ProjectToolbar.getBrushSize();
						g.setColor(active.getColor());
						g.drawOval((int)((xMouse -srcRect.x -brushSize/2)*magnification), (int)((yMouse - srcRect.y -brushSize/2)*magnification), (int)(brushSize * magnification), (int)(brushSize * magnification));
						break;
					case ProjectToolbar.PENCIL:
					case ProjectToolbar.WAND:
						final Composite co = g2d.getComposite();
						if (IJ.isWindows()) g2d.setColor(Color.yellow);
						else g2d.setXORMode(Color.yellow); // XOR on yellow for best contrast
						g2d.drawRect((int)((xMouse -srcRect.x -Segmentation.fmp.width/2)  * magnification),
							     (int)((yMouse -srcRect.y -Segmentation.fmp.height/2) * magnification),
							     (int)(Segmentation.fmp.width  * magnification),
							     (int)(Segmentation.fmp.height * magnification));
						g2d.setComposite(co); // undo XOR mode
						break;
				}
			}


			final Roi roi = imp.getRoi();
			if (null != roi) {
				roi.draw(g);
			}

			// Mathias code:
			if (null != freehandProfile) {
				freehandProfile.paint(g, magnification, srcRect, true);
				if(noCursor == null)
					noCursor = Toolkit.getDefaultToolkit().createCustomCursor(new BufferedImage(1,1,BufferedImage.TYPE_BYTE_BINARY), new Point(0,0), "noCursor");
			}

		} catch (final Exception e) {
			Utils.log2("DisplayCanvas.paint(Graphics) Error: " + e);
			IJError.print(e);
		} finally {
			synchronized (lock_paint) {
				lock_paint.unlock();
			}
		}
	}

	public void waitForRepaint() {
		// wait for all offscreen methods to finish painting
		RT.waitForOffs();
		// wait for the paint method to finish painting
		synchronized (lock_paint) {
			lock_paint.lock();
			lock_paint.unlock();
		}
	}

	/** Paints a handle on the screen coords. Adapted from ij.gui.Roi class. */
	static public void drawHandle(final Graphics g, final int x, final int y, final double magnification) {
		final int width5 = (int)(5 / magnification + 0.5);
		final int width3 = (int)(3 / magnification + 0.5);
		final int corr2 = (int)(2 / magnification + 0.5);
		final int corr1 = (int)(1 / magnification + 0.5);
		g.setColor(Color.white);
		g.drawRect(x - corr2, y - corr2, width5, width5);
		g.setColor(Color.black);
		g.drawRect(x - corr1, y - corr1, width3, width3);
		g.setColor(Color.white);
		g.fillRect(x, y, corr1, corr1);
	}

	/** Paints a handle at x,y screen coords. */
	static public void drawScreenHandle(final Graphics g, final int x, final int y) {
		g.setColor(Color.orange);
		g.drawRect(x - 2, y - 2, 5, 5);
		g.setColor(Color.black);
		g.drawRect(x - 1, y - 1, 3, 3);
		g.setColor(Color.orange);
		g.fillRect(x, y, 1, 1);
	}

	/** Paints a handle on the offscreen x,y. Adapted from ij.gui.Roi class. */
	/*
	private void drawHandle(Graphics g, double x, double y) {
		g.setColor(Color.black);
		g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
		g.setColor(Color.white);
		g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);
	}
	*/

	static protected BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
	static protected AffineTransform DEFAULT_AFFINE = new AffineTransform();

	static public void drawHandle(final Graphics2D g, final double x, final double y, final Rectangle srcRect, final double magnification) {
		final AffineTransform original = g.getTransform();
		g.setTransform(DEFAULT_AFFINE);
		final Stroke st = g.getStroke();
		g.setStroke(DEFAULT_STROKE);

		g.setColor(Color.black);
		g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
		g.setColor(Color.white);
		g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);

		g.setStroke(st);
		g.setTransform(original);
	}

	/** As offscreen. */
	private int x_p, y_p, x_d, y_d, x_d_old, y_d_old;

	private boolean popup = false;

	private boolean locked = false;

	private int tmp_tool = -1;

	/** In world coordinates. */
	protected Point last_popup = null;

	protected Point consumeLastPopupPoint() {
		final Point p = last_popup;
		last_popup = null;
		return p;
	}

	@Override
	public void mousePressed(final MouseEvent me) {

		super.flags = me.getModifiers();

		x_p = x_d = srcRect.x + (int) (me.getX() / magnification); // offScreenX(me.getX());
		y_p = y_d = srcRect.y + (int) (me.getY() / magnification); // offScreenY(me.getY());

		this.xMouse = x_p;
		this.yMouse = y_p;

		// ban if beyond bounds:
		if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
			return;
		}

		// Popup:
		popup = false; // not reset properly in macosx
		if (Utils.isPopupTrigger(me)) {
			popup = true;
			last_popup = new Point(x_p, y_p);
			display.getPopupMenu().show(this, me.getX(), me.getY());
			return;
		}

		// reset
		snapping = false;

		int tool = ProjectToolbar.getToolId();

		// pan with middle mouse like in inkscape
		/* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
		if (0 != (flags & InputEvent.BUTTON2_MASK))
		*/
		if (me.getButton() == MouseEvent.BUTTON2) {
			tmp_tool = tool;
			ProjectToolbar.setTool(Toolbar.HAND);
			tool = Toolbar.HAND;
		}
		//Utils.log2("button: " + me.getButton() + "  BUTTON2: " + MouseEvent.BUTTON2);

		if (!zoom_and_pan) {
			// stop animations when clicking (and on pushing ESC)
			cancelAnimations();
		}

		switch (tool) {
		case Toolbar.MAGNIFIER:
			if (me.isAltDown()) zoomOut(me.getX(), me.getY());
			else zoomIn(me.getX(), me.getY());
			return;
		case Toolbar.HAND:
			super.setupScroll(x_p, y_p); // offscreen coords.
			return;
		}

		if (input_disabled) {
			input_disabled2 = true;
			Utils.showMessage("Please wait while completing the task.\nOnly the glass and hand tool are enabled.");
			return; // only zoom and pan are allowed
		}

		Displayable active = display.getActive();

		if (isTransforming() && ProjectToolbar.SELECT != tool) {
			Utils.logAll("Notice: the 'Select' tool is not active!\n Activate the 'Select' tool to operate transformation modes.");
		}

		switch (tool) {
		case ProjectToolbar.PENCIL:
			if (null != active && active.isVisible() && active.getClass() == Profile.class) {
				final Profile prof = (Profile) active;
				this.freehandProfile = new FreeHandProfile(prof);
				freehandProfile.mousePressed(x_p, y_p);
				return;
			}
			break;
		case Toolbar.RECTANGLE:
		case Toolbar.OVAL:
		case Toolbar.POLYGON:
		case Toolbar.FREEROI:
		case Toolbar.LINE:
		case Toolbar.POLYLINE:
		case Toolbar.FREELINE:
		case Toolbar.ANGLE:
		case Toolbar.POINT:
			// pass the mouse event to superclass ImageCanvas.
			super.mousePressed(me);
			repaint();
			return;
		case Toolbar.DROPPER:
			// The color dropper
			setDrawingColor(x_p, y_p, me.isAltDown());
			return;
		}

		// check:
		if (display.isReadOnly()) return;

		switch (tool) {
		case Toolbar.TEXT:
			if (!isTransforming()) {
				// edit a label, or add a new one
				if (null == active || !active.contains(x_p, y_p)) {
					// find a Displayable to activate, if any
					display.choose(me.getX(), me.getY(), x_p, y_p, DLabel.class);
					active = display.getActive();
				}
				if (null != active && active.isVisible() && active instanceof DLabel) {
					// edit
					((DLabel) active).edit();
				} else {
					// new
					final DLabel label = new DLabel(display.getProject(), "  ", x_p, y_p);
					display.getLayer().add(label);
					label.edit();
				}
			}
			return;
		}

		// SPECIFIC for SELECT and above tools

		// no ROIs allowed past this point
		if (tool >= ProjectToolbar.SELECT) imp.killRoi();
		else return;

		Selection selection = display.getSelection();
		if (isTransforming()) {
			box = display.getMode().getRepaintBounds();
			display.getMode().mousePressed(me, x_p, y_p, magnification);
			return;
		}
		// select or deselect another active Displayable, or add it to the selection group:
		if (ProjectToolbar.SELECT == tool) {
			display.choose(me.getX(), me.getY(), x_p, y_p, me.isShiftDown(), null);
		}
		active = display.getActive();
		selection = display.getSelection();

		if (null == active || !active.isVisible()) return;

		switch (tool) {
		case ProjectToolbar.SELECT:
			// gather initial box (for repainting purposes)
			box = display.getMode().getRepaintBounds();
			// check if the active is usable:
			// check if the selection contains locked objects
			if (selection.isLocked()) {
				locked = true;
				return;
			}
			display.getMode().mousePressed(me, x_p, y_p, magnification);
			break;
		default: // the PEN and PENCIL tools, and any other custom tool
			display.getLayerSet().addPreDataEditStep(active);
			box = active.getBoundingBox();
			active.mousePressed(me, display.getLayer(), x_p, y_p, magnification);
			invalidateVolatile();
			break;
		}
	}

	@Override
	public void mouseDragged(final MouseEvent me) {

		super.flags = me.getModifiers();

		if (popup) return;

		// ban if beyond bounds:
		if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
			return;
		}

		if (ProjectToolbar.SELECT == ProjectToolbar.getToolId() && locked) {
			Utils.log2("Selection is locked.");
			return;
		}

		dragging = true;

		x_d_old = x_d;
		y_d_old = y_d;

		x_d = srcRect.x + (int) (me.getX() / magnification); // offscreen
		y_d = srcRect.y + (int) (me.getY() / magnification);

		this.xMouse = x_d;
		this.yMouse = y_d;

		// protection:
		final int me_x = me.getX();
		final int me_y = me.getY();
		if (me_x < 0 || me_x > this.getWidth() || me_y < 0 || me_y > this.getHeight()) {
			x_d = x_d_old;
			y_d = y_d_old;
			return;
		}

		final int tool = ProjectToolbar.getToolId();


		// pan with middle mouse like in inkscape
		/* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
		if (0 != (flags & InputEvent.BUTTON2_MASK)) {
			tool = Toolbar.HAND;
		}
		*/ // so the above has been implemented as a temporary switch to the HAND tool at the mousePressed function.

		switch (tool) {
		case Toolbar.MAGNIFIER: // TODO : create a zooms-area tool
			return;
		case Toolbar.HAND:
			final int srx = srcRect.x,
			    sry = srcRect.y;
			scroll(me.getX(), me.getY());
			if (0 != srx - srcRect.x || 0 != sry - srcRect.y) {
				update_graphics = true; // update the offscreen images.
				display.getNavigator().repaint(false);
				repaint(true);
			}
			return;
		}

		if (input_disabled2) return;

		//debug:
		//Utils.log2("x_d,y_d : " + x_d + "," + y_d + "   x_d_old, y_d_old : " + x_d_old + "," + y_d_old + "  dx, dy : " + (x_d_old - x_d) + "," + (y_d_old - y_d));

		// Code for Matthias' FreehandProfile (TODO this should be done on mousePressed, not on mouseDragged)

		final Displayable active = display.getActive();
		if (null != active && active.getClass() == Profile.class) {
			try {
				if (r == null) {
					r = new Robot(this.getGraphicsConfiguration().getDevice());
				}
			} catch (final AWTException e) {
				e.printStackTrace();
			}
		}

		switch (tool) {
		case ProjectToolbar.PENCIL:
			if (null != active && active.isVisible() && active.getClass() == Profile.class) {
				if (freehandProfile == null)
					return; // starting painting out of the DisplayCanvas border
				final double dx = x_d - x_d_old;
				final double dy = y_d - y_d_old;
				freehandProfile.mouseDragged(me, x_d, y_d, dx, dy);
				repaint();
				// Point screenLocation = getLocationOnScreen();
				// mousePos[0] += screenLocation.x;
				// mousePos[1] += screenLocation.y;
				// r.mouseMove( mousePos[0], mousePos[1]);
				return;
			}
			break;
		case Toolbar.RECTANGLE:
		case Toolbar.OVAL:
		case Toolbar.POLYGON:
		case Toolbar.FREEROI:
		case Toolbar.LINE:
		case Toolbar.POLYLINE:
		case Toolbar.FREELINE:
		case Toolbar.ANGLE:
		case Toolbar.POINT:
			// pass the mouse event to superclass ImageCanvas.
			super.mouseDragged(me);
			repaint(false);
			return;
		}
		// no ROIs beyond this point
		if (tool >= ProjectToolbar.SELECT) imp.killRoi();
		else return;

		// check:
		if (display.isReadOnly()) return;

		if (null != active && active.isVisible()) {
			// prevent dragging beyond the layer limits
			if (display.getLayer().contains(x_d, y_d, 1)) {
				Rectangle box2;
				switch (tool) {
				case ProjectToolbar.SELECT:
					display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
					box2 = display.getMode().getRepaintBounds();
					box.add(box2);
					// repaint all Displays (where it was and where it is now, hence the sum of both boxes):
					Display.repaint(display.getLayer(), Selection.PADDING, box, false, active.isLinked() || active.getClass() == Patch.class);
					// box for next mouse dragged iteration
					box = box2;
					break;
				default:
					active.mouseDragged(me, display.getLayer(), x_p, y_p, x_d, y_d, x_d_old, y_d_old);
					// the line above must repaint on its own
					break;
				}
			} else {
				//beyond_srcRect = true;
				Utils.log("DisplayCanvas.mouseDragged: preventing drag beyond layer limits.");
			}
		} else if (display.getMode() instanceof ManualAlignMode
			|| display.getMode() instanceof InspectPatchTrianglesMode) {
			if (display.getLayer().contains(x_d, y_d, 1)) {
				if (tool >= ProjectToolbar.SELECT) {
					display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
				}
			}
		}
	}

	@Override
	public void mouseReleased(final MouseEvent me) {

		super.flags = me.getModifiers();

		final boolean dragging2 = dragging;
		dragging = false;
		final boolean locked2 = locked;
		locked = false;

		if (popup) {
			popup = false;
			return;
		}

		// ban if beyond bounds:
		if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
			return;
		}

		final int tool = ProjectToolbar.getToolId();

		// pan with middle mouse like in inkscape
		/* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
		if (0 != (flags & InputEvent.BUTTON2_MASK)) {
			tool = Toolbar.HAND;
		}
		*/

		switch (tool) {
		case Toolbar.MAGNIFIER:
			// display.updateInDatabase("srcRect"); // TODO if the display.frame
			// is shrinked, the pack() in the zoom methods will also call the
			// updateInDatabase("srcRect") (so it's going to be done twice)
			display.updateFrameTitle();
			return;
		case Toolbar.HAND:
			display.updateInDatabase("srcRect");
			if (-1 != tmp_tool) {
				ProjectToolbar.setTool(tmp_tool);
				tmp_tool = -1;
			}
			if (!dragging2) repaint(true); // TEMPORARY just to allow fixing bad screen when simply cliking with the hand
			display.getMode().srcRectUpdated(srcRect, magnification);
			return;
		}

		if (input_disabled2) {
			input_disabled2 = false; // reset
			return;
		}

		if (locked2) {
			if (ProjectToolbar.SELECT == tool) {
				if (dragging2) {
					Utils.showMessage("Selection is locked!");
				}
				return;
			}
		}

		// pan with middle mouse like in inkscape
		/* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
		if (0 != (flags & InputEvent.BUTTON2_MASK)) {
			tool = Toolbar.HAND;
		}
		*/

		super.flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set
		super.flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set
		super.flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set

		final int x_r = srcRect.x + (int)(me.getX() / magnification);
		final int y_r = srcRect.y + (int)(me.getY() / magnification);

		/*
		if (beyond_srcRect) {
			// Artificial release on the last dragged point
			x_r = x_d;
			y_r = y_d;
		}
		*/

		this.xMouse = x_r;
		this.yMouse = y_r;

		final Displayable active = display.getActive();

		switch (tool) {
			case ProjectToolbar.PENCIL:
				if (null != active && active.isVisible() && active.getClass() == Profile.class) {
					if (freehandProfile == null)
						return; // starting painting out of the DisplayCanvas boarder
					freehandProfile.mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
					freehandProfile = null;
					//repaint(true);
					final Selection selection = display.getSelection();
					selection.updateTransform(display.getActive());
					Display.repaint(display.getLayer(), selection.getBox(), Selection.PADDING); // repaints the navigator as well
					return;
				}
				break;
			case Toolbar.RECTANGLE:
			case Toolbar.OVAL:
			case Toolbar.POLYGON:
			case Toolbar.FREEROI:
			case Toolbar.LINE:
			case Toolbar.POLYLINE:
			case Toolbar.FREELINE:
			case Toolbar.ANGLE:
			case Toolbar.POINT:
				// pass the mouse event to superclass ImageCanvas.
				super.mouseReleased(me);
				repaint();
				// return; // replaced by #SET_ROI
		}

		final Roi roi = imp.getRoi();

		// check:
		if (display.isReadOnly()) return;

		if (tool >= ProjectToolbar.SELECT) {
			if (null != roi) imp.killRoi();
		} else return; // #SET_ROI

		final Selection selection = display.getSelection();

		if (snapping) {
			// finish dragging
			display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
			box.add(display.getMode().getRepaintBounds());
			Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
			Display.snap((Patch)active);
			// reset:
			snapping = false;
			return;
		}

		if (null != active && active.isVisible()) {
			switch(tool) {
			case ProjectToolbar.SELECT:
				display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
				box.add(display.getMode().getRepaintBounds());
				Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
				break;
			case ProjectToolbar.PENCIL:
			case ProjectToolbar.PEN:
			case ProjectToolbar.BRUSH:
				active.mouseReleased(me, display.getLayer(), x_p, y_p, x_d, y_d, x_r, y_r); // active, not selection (Selection only handles transforms, not active's data editions)
				// update active's bounding box
				selection.updateTransform(active);
				box.add(selection.getBox());
				Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
				//if (!active.getClass().equals(AreaList.class)) Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
				// TODO: this last repaint call is unnecessary, if the box was properly repainted on mouse drag for Profile etc.
				//else
				if (null != old_brush_box) {
					repaint(old_brush_box, 0, false);
					old_brush_box = null; // from mouseMoved
				}
				// The current state:
				display.getLayerSet().addDataEditStep(active);
				break;
			}
		}
	}

	private boolean mouse_in = false;

	@Override
	public void mouseEntered(final MouseEvent me) {
		mouse_in = true;
		// try to catch focus if the JFrame is front most
		if (display.isActiveWindow() && !this.hasFocus()) {
			this.requestFocus();
		}
		// bring dragged point to mouse pointer
		// TODO doesn't work as expected.
		/*
		Displayable active = display.getActive();
		int x = offScreenX(me.getX());
		int y = offScreenY(me.getY());
		if (null != active) {
			active.snapTo(x, y, x_p, y_p);
			x_p = x_d = x_d_old = x;
			y_p = y_d = y_d_old = y;
		}
		*/
		//Utils.log2("mouseEntered x,y: " + offScreenX(me.getX()) + "," + offScreenY(me.getY()));
	}

	@Override
	public void mouseExited(final MouseEvent me) {
		mouse_in = false;
		// paint away the circular brush if any
		if (ProjectToolbar.getToolId() == ProjectToolbar.BRUSH) {
			final Displayable active = display.getActive();
			if (null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
				if (null != old_brush_box) {
					this.repaint(old_brush_box, 0);
					old_brush_box = null;
				}
			}
		}
	}

	/** Sets the cursor based on the current tool and cursor location. */
	@Override
	public void setCursor(final int sx, final int sy, final int ox, final int oy) {
		// copy of ImageCanvas.setCursor without the win==null
		xMouse = ox;
		yMouse = oy;
		final Roi roi = imp.getRoi();
		/*
		 * ImageWindow win = imp.getWindow(); if (win==null) return;
		 */
		if (IJ.spaceBarDown()) {
			setCursor(handCursor);
			return;
		}
		switch (Toolbar.getToolId()) {
		case Toolbar.MAGNIFIER:
			if (IJ.isMacintosh())
				setCursor(defaultCursor);
			else
				setCursor(moveCursor);
			break;
		case Toolbar.HAND:
			setCursor(handCursor);
			break;
		case ProjectToolbar.SELECT:
		case ProjectToolbar.PENCIL:
			setCursor(defaultCursor);
			break;
		default: // selection tool

			if (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.isHandle(sx, sy) >= 0)
				setCursor(handCursor);
			else if (Prefs.usePointerCursor || (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.contains(ox, oy)))
				setCursor(defaultCursor);
			else
				setCursor(crosshairCursor);
			break;
		}
	}
	/** Set the srcRect - used by the DisplayNavigator. */
	protected void setSrcRect(final int x, final int y, int width, int height) {
		if (width < 1) width = 1;
		if (height < 1) height = 1;
		this.srcRect.setRect(x, y, width, height);
		display.updateInDatabase("srcRect");
		display.getMode().srcRectUpdated(srcRect, magnification);
	}

	@Override
	public void setDrawingSize(final int new_width, final int new_height) {
		adjustSrcRect(new_width, new_height);
		super.setDrawingSize(new_width, new_height);
	}

	/** Adjust srcRect and internal variables to the canvas' bounds. */
	public void adjustDimensions() {
		final Rectangle r = getBounds();
		adjustSrcRect(r.width, r.height);
		super.dstWidth = r.width;
		super.dstHeight = r.height;
	}

	/** Adjust srcRect to new dimensions. */
	public void adjustSrcRect(final int new_width, final int new_height) {
		final double mag = super.getMagnification();
		// This method is very important! Make it fit perfectly.
		if (srcRect.width * mag < new_width) {
			// expand
			if (new_width > imageWidth * mag) {
				// too large, limit
				srcRect.x = 0;
				srcRect.width = imageWidth;
			} else {
				srcRect.width = (int) Math.ceil(new_width / mag);
				if (srcRect.x + srcRect.width > imageWidth) {
					srcRect.x = imageWidth - srcRect.width;
				}
			}
		} else {
			// shrink
			srcRect.width = (int) Math.ceil(new_width / mag);
		}
		if (srcRect.height * mag < new_height) {
			// expand
			if (new_height > imageHeight * mag) {
				// too large, limit
				srcRect.y = 0;
				srcRect.height = imageHeight;
			} else {
				srcRect.height = (int) Math.ceil(new_height / mag);
				if (srcRect.y + srcRect.height > imageHeight) {
					srcRect.y = imageHeight - srcRect.height;
				}
			}
		} else {
			// shrink
			srcRect.height = (int) Math.ceil(new_height / mag);
		}
	}

	private void zoomIn2(int x, int y) {
		// copy of ImageCanvas.zoomIn except for the canEnlarge is different and
		// there's no call to the non-existing ImageWindow
		if (magnification >= 32)
			return;
		final double newMag = getHigherZoomLevel2(magnification);

		// zoom at point: correct mag drift
		final int cx = getWidth() / 2;
		final int cy = getHeight() / 2;
		final int dx = (int)(((x - cx) * magnification) / newMag);
		final int dy = (int)(((y - cy) * magnification) / newMag);
		x -= dx;
		y -= dy;

		// Adjust the srcRect to the new dimensions
		int w = (int) Math.round(dstWidth / newMag);
		if (w * newMag < dstWidth)
			w++;
		if (w > imageWidth)
			w = imageWidth;
		int h = (int) Math.round(dstHeight / newMag);
		if (h * newMag < dstHeight)
			h++;
		if (h > imageHeight)
			h = imageHeight;
		x = offScreenX(x);
		y = offScreenY(y);
		final Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
		if (r.x < 0)
			r.x = 0;
		if (r.y < 0)
			r.y = 0;
		if (r.x + w > imageWidth)
			r.x = imageWidth - w;
		if (r.y + h > imageHeight)
			r.y = imageHeight - h;
		srcRect = r;

		//display.pack();

		setMagnification(newMag);
		display.updateInDatabase("srcRect");
		display.repaintAll2(); // this repaint includes this canvas's repaint as well, but also the navigator, etc. // repaint();
	}

	private void zoomOut2(int x, int y) {
		//if (magnification <= 0.03125)
		//	return;
		final double newMag = getLowerZoomLevel2(magnification);

		// zoom at point: correct mag drift
		final int cx = getWidth() / 2;
		final int cy = getHeight() / 2;
		final int dx = (int)(((x - cx) * magnification) / newMag);
		final int dy = (int)(((y - cy) * magnification) / newMag);
		x -= dx;
		y -= dy;

		if (imageWidth * newMag > dstWidth || imageHeight * newMag > dstHeight) {
			int w = (int) Math.round(dstWidth / newMag);
			if (w * newMag < dstWidth)
				w++;
			int h = (int) Math.round(dstHeight / newMag);
			if (h * newMag < dstHeight)
				h++;
			x = offScreenX(x);
			y = offScreenY(y);
			final Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
			if (r.x < 0)
				r.x = 0;
			if (r.y < 0)
				r.y = 0;
			if (r.x + w > imageWidth)
				r.x = imageWidth - w;
			if (r.y + h > imageHeight)
				r.y = imageHeight - h;
			srcRect = r;
		} else {
			// Shrink srcRect, but NOT the dstWidth,dstHeight of the canvas, which remain the same:
			srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
		}

		setMagnification(newMag);
		display.repaintAll2(); // this repaint includes this canvas's repaint, but updates the navigator without update_graphics
		display.updateInDatabase("srcRect");
	}

	/** The minimum amout of pixels allowed for width or height when zooming out. */
	static private final int MIN_DIMENSION = 10; // pixels

	/** Enable zooming out up to the point where the display becomes 10 pixels in width or height. */
	protected double getLowerZoomLevel2(final double currentMag) {
		// if it is 1/72 or lower, then:
		if (Math.abs(currentMag - 1/72.0) < 0.00000001 || currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
			// find nearest power of two under currentMag
			// start at level 7, which is 1/128
			int level = 7;
			double scale = currentMag;
			while (scale * srcRect.width > MIN_DIMENSION && scale * srcRect.height > MIN_DIMENSION) {
				scale = 1 / Math.pow(2, level);
				// if not equal and actually smaller, break:
				if (Math.abs(scale - currentMag) != 0.00000001 && scale < currentMag) break;
				level++;
			}
			return scale;
		} else {
			return ImageCanvas.getLowerZoomLevel(currentMag);
		}
	}
	protected double getHigherZoomLevel2(final double currentMag) {
		// if it is not 1/72 and its lower, then:
		if (Math.abs(currentMag - 1/72.0) > 0.00000001 && currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
			// find nearest power of two above currentMag
			// start at level 14, which is 0.00006103515625 (0.006 %)
			int level = 14; // this value may be increased in the future
			double scale = currentMag;
			while (level >= 0) {
				scale = 1 / Math.pow(2, level);
				if (scale > currentMag) break;
				level--;
			}
			return scale;
		} else {
			return ImageCanvas.getHigherZoomLevel(currentMag);
		}
	}


	/*
	 * // OBSOLETE: modified ij.gui.ImageCanvas directly
	 public void mouseMoved(MouseEvent e) { if (IJ.getInstance()==null) return; int sx =
	 * e.getX(); int sy = e.getY(); int ox = offScreenX(sx); int oy =
	 * offScreenY(sy); flags = e.getModifiers(); setCursor(sx, sy, ox, oy);
	 * IJ.setInputEvent(e); Roi roi = imp.getRoi(); if (roi!=null &&
	 * (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE ||
	 * roi.getType()==Roi.ANGLE) && roi.getState()==roi.CONSTRUCTING) {
	 * PolygonRoi pRoi = (PolygonRoi)roi; pRoi.handleMouseMove(ox, oy); } else {
	 * if (ox<imageWidth && oy<imageHeight) { //ImageWindow win =
	 * imp.getWindow(); //if (win!=null) win.mouseMoved(ox, oy);
	 * imp.mouseMoved(ox, oy); } else IJ.showStatus(""); } }
	 */

	private Rectangle old_brush_box = null;

	private MouseMovedThread mouse_moved = new MouseMovedThread();

	private class MouseMovedThread extends Thread {
		private volatile MouseEvent me = null;
		MouseMovedThread() {
			super("T2-mouseMoved");
			setDaemon(true);
			setPriority(Thread.NORM_PRIORITY);
			start();
		}
		void dispatch(final MouseEvent me) {
			//Utils.log2("before");
			synchronized (this) {
				//Utils.log2("in");
				this.me = me;
				notifyAll();
			}
		}
		void quit() {
			interrupt();
			synchronized (this) { notifyAll(); }
		}
		@Override
		public void run() {
			while (!isInterrupted()) {
				MouseEvent me = this.me;
				if (null != me) {
					try { mouseMoved(me); } catch (final Exception e) { IJError.print(e); }
				}
				// Wait only if the event has not changed
				synchronized (this) {
					if (me == this.me) {
						// Release the pointer
						me = null;
						this.me = null;
						if (isInterrupted()) return;
						// Wait until there is a new event
						try { wait(); } catch (final Exception e) {}
					}
				}
			}
		}
		private void mouseMoved(final MouseEvent me) {
			if (null == me) return;

			if (input_disabled || display.getMode().isDragging()) return;

			xMouse = (int)(me.getX() / magnification) + srcRect.x;
			yMouse = (int)(me.getY() / magnification) + srcRect.y;
			final Displayable active = display.getActive();

			// only when no mouse buttons are down
			final int flags = DisplayCanvas.super.flags;
			if (0 == (flags & InputEvent.BUTTON1_MASK)
			/* && 0 == (flags & InputEvent.BUTTON2_MASK) */ // this is the alt key down ..
			 && 0 == (flags & InputEvent.BUTTON3_MASK)
			//if (me.getButton() == MouseEvent.NOBUTTON
			 && null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
				final int tool = ProjectToolbar.getToolId();
				Rectangle r = null;
				if (ProjectToolbar.BRUSH == tool) {
					// repaint area where the brush circle is
					final int brushSize = ProjectToolbar.getBrushSize() +2; // +2 padding
					r = new Rectangle( xMouse - brushSize/2,
							   yMouse - brushSize/2,
							   brushSize+1,
							   brushSize+1 );
				} else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
					// repaint area where the fast-marching box is
					r = new Rectangle( xMouse - Segmentation.fmp.width/2 - 2,
							   yMouse - Segmentation.fmp.height/2 - 2,
							   Segmentation.fmp.width + 4,
							   Segmentation.fmp.height + 4 );
				}
				if (null != r) {
					final Rectangle copy = (Rectangle)r.clone();
					if (null != old_brush_box) r.add(old_brush_box);
					old_brush_box = copy;
					repaint(r, 1); // padding because of painting rounding which would live dirty trails
				}
			}

			if (me.isShiftDown()) {
				// Print a comma-separated list of objects under the mouse pointer
				final Layer layer = DisplayCanvas.this.display.getLayer();
				final List<Displayable> al = getDisplayablesUnderMouse(me);
				if (0 == al.size()) {
					Utils.showStatus("", false);
					return;
				}
				final StringBuilder sb = new StringBuilder();
				final Project pr = layer.getProject();
				for (final Displayable d : al) sb.append(pr.getShortMeaningfulTitle(d)).append(", ");
				sb.setLength(sb.length()-2);
				Utils.showStatus(sb.toString(), false);
			} else {
				// For very large images, the Patch.getPixel can take even half a minute
				// to do the pixel grab operation.
				//DisplayCanvas.super.mouseMoved(me);
				// Instead, find out over what are we
				final List<Displayable> under = getDisplayablesUnderMouse(me);
				final Calibration cal = display.getLayerSet().getCalibration();
				if (under.isEmpty()) {
					Utils.showStatus("x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
													 + ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit());
					return;
				}
				final Displayable top = under.get(0);
				String msg =
							"x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
							+ ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit();
				if (top.getClass() == Patch.class) {
					final Patch patch = (Patch)top;
					final int[] p = new int[4];
					BufferedImage offsc;
					synchronized (offscreen_lock) {
						offsc = offscreen;
					}
					if (null == offsc) return;
					try {
						final PixelGrabber pg = new PixelGrabber(offsc, me.getX(), me.getY(), 1, 1, p, 0, offsc.getWidth(null));
						pg.grabPixels();
					} catch (final InterruptedException ie) {
						IJError.print(ie);
						return;
					} catch (final Throwable t) {
						// The offscreen might have been flushed. Just ignore; pixel value will be reported next.
						return;
					}
					patch.approximateTransferPixel(p);
					msg += ", value=";
					switch (patch.getType()) {
						case ImagePlus.GRAY16:
						case ImagePlus.GRAY8:
							msg += p[0];
							break;
						case ImagePlus.GRAY32:
							msg += Float.intBitsToFloat(p[0]);
							break;
						case ImagePlus.COLOR_RGB:
						case ImagePlus.COLOR_256:
							msg += "(" + p[0] + "," + p[1] + "," + p[2] + ")";
							break;
					}
					msg += " [Patch #" + patch.getId() + "]";
				} else {
					final Color c = top.getColor();
					msg += ", value=[" + c.getRed() + "," + c.getGreen() + "," + c.getBlue() + "] [" + Project.getName(top.getClass()) + " #" + top.getId() + "]";
				}
				Utils.showStatus(msg);
			}
		}
	}

	/** See {@link DisplayCanvas#getDisplayablesUnderMouse(MouseEvent)}. */
	public List<Displayable> getDisplayablesUnderMouse() {
		return getDisplayablesUnderMouse(new MouseEvent(this, -1, 0, 0, xMouse, yMouse, 1, false));
	}

	/** Return the list of Displayable objects under the mouse,
	 * sorted by proper stack order. */
	public List<Displayable> getDisplayablesUnderMouse(final MouseEvent me) {
				final Layer layer = display.getLayer();
				final int x_p = offScreenX(me.getX()),
				          y_p = offScreenY(me.getY());
				final ArrayList<Displayable> al = new ArrayList<Displayable>(layer.getParent().findZDisplayables(layer, x_p, y_p, true));
				Collections.reverse(al);
				final ArrayList<Displayable> al2 = new ArrayList<Displayable>(layer.find(x_p, y_p, true));
				Collections.reverse(al2);
				al.addAll(al2);
				return al;
	}

	public boolean isDragging() {
		return display.getMode().isDragging();
	}

	@Override
	public void mouseMoved(final MouseEvent me) {
		super.flags = me.getModifiers();
		final int tool = Toolbar.getToolId();
		switch (tool) {
		case Toolbar.POLYLINE:
		case Toolbar.POLYGON:
		case Toolbar.ANGLE:
			super.mouseMoved(me);
			repaint();
			return;
		}
		mouse_moved.dispatch(me);
	}

	/** Zoom in using the current mouse position, or the center if the mouse is out. */
	public void zoomIn() {
		if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
			zoomIn(dstWidth/2, dstHeight/2);
		} else {
			zoomIn(screenX(xMouse), screenY(yMouse));
		}
	}

	/** Overriding to repaint the DisplayNavigator as well. */
	@Override
	public void zoomIn(final int x, final int y) {
		update_graphics = true; // update the offscreen images.
		zoomIn2(x, y);
	}

	/** Zoom out using the current mouse position, or the center if the mouse is out. */
	public void zoomOut() {
		if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
			zoomOut(dstWidth/2, dstHeight/2);
		} else zoomOut(screenX(xMouse), screenY(yMouse));
	}

	/** Overriding to repaint the DisplayNavigator as well. */
	@Override
	public void zoomOut(final int x, final int y) {
		update_graphics = true; // update the offscreen images.
		zoomOut2(x, y);
	}

	/** Center the srcRect around the given object(s) bounding box, zooming if necessary,
	 *  so that the given r becomes a rectangle centered in the srcRect and zoomed out by a factor of 2. */
	public void showCentered(final Rectangle r) {
		// multiply bounding box dimensions by two
		r.x -= r.width / 2;
		r.y -= r.height / 2;
		r.width += r.width;
		r.height += r.height;
		// compute target magnification
		final double magn = getWidth() / (double)(r.width > r.height ? r.width : r.height);
		center(r, magn);
	}

	/** Show the given r as the srcRect (or as much of it as possible) at the given magnification. */
	public void center(final Rectangle r, double magn) {
		// bring bounds within limits of the layer and the canvas' drawing size
		final double lw = display.getLayer().getLayerWidth();
		final double lh = display.getLayer().getLayerHeight();
		final int cw = (int) (getWidth() / magn); // canvas dimensions in offscreen coords
		final int ch = (int) (getHeight() / magn);

		// 2nd attempt:
		// fit to canvas drawing size:
		r.y += (r.height - ch) / 2;
		r.width = cw;
		r.height = ch;
		// place within layer bounds
		if (r.x < 0) r.x = 0;
		if (r.y < 0) r.y = 0;
		if (r.width > lw) {
			r.x = 0;
			r.width = (int)lw;
		}
		if (r.height > lh) {
			r.y = 0;
			r.height = (int)lh;
		}
		if (r.x + r.width > lw) r.x = (int)(lw - cw);
		if (r.y + r.height > lh) r.y = (int)(lh - ch);
		// compute magn again, since the desired width may have changed:
		magn = getWidth() / (double)r.width;

		// set magnification and srcRect
		setup(magn, r);
		try { Thread.sleep(200); } catch (final Exception e) {} // swing ... waiting for the display.pack()
		update_graphics = true;
		RT.paint(null, update_graphics);
		display.updateInDatabase("srcRect");
		display.updateFrameTitle();
		display.getNavigator().repaint(false);
	}

	/** Repaint as much as the bounding box around the given Displayable. If the Displayable is null, the entire canvas is repainted, remaking the offscreen images. */
	public void repaint(final Displayable d) {
		repaint(d, 0);
	}

	/**
	 * Repaint as much as the bounding box around the given Displayable plus the
	 * extra padding. If the Displayable is null, the entire canvas is
	 * repainted, remaking the offscreen images.
	 */
	public void repaint(final Displayable displ, final int extra) {
		repaint(displ, extra, update_graphics);
	}
	public void repaint(final Displayable displ, final int extra, final boolean update_graphics) {
		if (null != displ) {
			final Rectangle r = displ.getBoundingBox();
			r.x = (int) ((r.x - srcRect.x) * magnification) - extra;
			r.y = (int) ((r.y - srcRect.y) * magnification) - extra;
			r.width = (int) Math.ceil(r.width * magnification) + extra + extra;
			r.height = (int) Math.ceil(r.height * magnification) + extra + extra;
			invalidateVolatile();
			RT.paint(r, update_graphics);
		} else {
			// everything
			repaint(true);
		}
	}

	/**
	 * Repaint the clip corresponding to the sum of all boundingboxes of
	 * Displayable objects in the hashset.
	 */
	// it is assumed that the linked objects are close to each other, otherwise
	// the clip rectangle grows enormously.
	public void repaint(final HashSet<Displayable> hs) {
		if (null == hs) return;
		Rectangle r = null;
		final Layer dl = display.getLayer();
		for (final Displayable d : hs) {
			if (d.getLayer() == dl) {
				if (null == r) r = d.getBoundingBox();
				else r.add(d.getBoundingBox());
			}
		}
		if (null != r) {
			//repaint(r.x, r.y, r.width, r.height);
			invalidateVolatile();
			RT.paint(r, update_graphics);
		}
	}

	/**
	 * Repaint the given offscreen Rectangle after transforming its data on the fly to the
	 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
	 * modified.
	 */
	public void repaint(final Rectangle r, final int extra) {
		invalidateVolatile();
		if (null == r) {
			//Utils.log2("DisplayCanvas.repaint(Rectangle, int) warning: null r");
			RT.paint(null, update_graphics);
			return;
		}
		// repaint((int) ((r.x - srcRect.x) * magnification) - extra, (int) ((r.y - srcRect.y) * magnification) - extra, (int) Math .ceil(r.width * magnification) + extra + extra, (int) Math.ceil(r.height * magnification) + extra + extra);
		RT.paint(new Rectangle((int) ((r.x - srcRect.x) * magnification) - extra, (int) ((r.y - srcRect.y) * magnification) - extra, (int) Math.ceil(r.width * magnification) + extra + extra, (int) Math.ceil(r.height * magnification) + extra + extra), update_graphics);
	}

	/**
	 * Repaint the given Rectangle after transforming its data on the fly to the
	 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
	 * modified.
	 * @param box The rectangle to repaint
	 * @param extra The extra outbound padding to add to the rectangle
	 * @param update_graphics Whether to recreate the offscreen images or not
	 */
	public void repaint(final Rectangle box, final int extra, final boolean update_graphics) {
		this.update_graphics = update_graphics;
		repaint(box, extra);
	}

	/** Repaint everything, updating offscreen graphics if so specified. */
	public void repaint(final boolean update_graphics) {
		this.update_graphics = update_graphics | this.update_graphics;
		invalidateVolatile();
		RT.paint(null, this.update_graphics);
	}

	/** Overridden to multithread. This method is here basically to enable calls to the FakeImagePlus.draw from the HAND and other tools to repaint properly.*/
	@Override
	public void repaint() {
		//Utils.log2("issuing thread");
		invalidateVolatile();
		RT.paint(null, update_graphics);
	}

	/** Overridden to multithread. */
	/* // saved as unoveridden to make sure there are no infinite thread loops when calling super in buggy JVMs
	public void repaint(long ms, int x, int y, int width, int height) {
		RT.paint(new Rectangle(x, y, width, height), update_graphics);
	}
	*/

	/** Overridden to multithread. */
	@Override
	public void repaint(final int x, final int y, final int width, final int height) {
		invalidateVolatile();
		RT.paint(new Rectangle(x, y, width, height), update_graphics);
	}

	public void setUpdateGraphics(final boolean b) {
		update_graphics = b;
	}

	/** Release offscreen images and stop threads. */
	public void flush() {
		// cleanup update graphics thread if any
		RT.quit();
		synchronized (offscreen_lock) {
			if (null != offscreen) {
				offscreen.flush();
				offscreen = null;
			}
			update_graphics = true;
			for (final BufferedImage bi : to_flush) bi.flush();
			to_flush.clear();
		}
		mouse_moved.quit();
		try {
			synchronized (this) { if (null != animator) animator.shutdownNow(); }
			cancelAnimations();
		} catch (final Exception e) {}
		animator = null;
	}

	public void destroy() {
		flush();
		WindowManager.setTempCurrentImage(imp); // the FakeImagePlus
		WindowManager.removeWindow(fake_win); // the FakeImageWindow
	}

	public boolean applyTransform() {
		final boolean b = display.getMode().apply();
		if (b) {
			display.setMode(new DefaultMode(display));
			Display.repaint();
		}
		return b;
	}

	public boolean isTransforming() {
		// TODO this may have to change if modes start getting used for a task other than transformation.
		// Perhaps "isTransforming" will have to broaden its meaning to "isNotDefaultMode"
		return display.getMode().getClass() != DefaultMode.class;
	}

	public void cancelTransform() {
		display.getMode().cancel();
		display.setMode(new DefaultMode(display));
		repaint(true);
	}

	private int last_keyCode = KeyEvent.VK_ESCAPE;
	private boolean tagging = false;

	@Override
	public void keyPressed(final KeyEvent ke) {

		final Displayable active = display.getActive();

		if (null != freehandProfile
			&& ProjectToolbar.getToolId() == ProjectToolbar.PENCIL
			&& ke.getKeyCode() == KeyEvent.VK_ESCAPE
			&& null != freehandProfile)
		{
			freehandProfile.abort();
			ke.consume();
			return;
		}

		final int keyCode = ke.getKeyCode();

		try {
			// Enable tagging system for any alphanumeric key:
			if (!input_disabled && null != active && active instanceof Tree<?> && ProjectToolbar.isDataEditTool(ProjectToolbar.getToolId())) {
				if (tagging) {
					if (KeyEvent.VK_0 == keyCode && KeyEvent.VK_0 != last_keyCode) {
						// do nothing: keep tagging as true
					} else {
						// last step of tagging: a char after t or after t and a number (and the char itself can be a number)
						tagging = false;
					}
					active.keyPressed(ke);
					return;
				} else if (KeyEvent.VK_T == keyCode) {
					tagging = true;
					active.keyPressed(ke);
					return;
				}
			}
		} finally {
			last_keyCode = keyCode;
		}

		tagging = false;

		if (ke.isConsumed()) return;

		/*
		 * TODO screen editor ... TEMPORARY if (active instanceof DLabel) {
		 * active.keyPressed(ke); ke.consume(); return; }
		 */

		if (!zoom_and_pan) {
			if (KeyEvent.VK_ESCAPE == keyCode) {
				cancelAnimations();
			}
			return;
		}

		final int keyChar = ke.getKeyChar();

		boolean used = false;

		switch (keyChar) {
		case '+':
		case '=':
			zoomIn();
			used = true;
			break;
		case '-':
		case '_':
			zoomOut();
			used = true;
			break;
		default:
			break;
		}


		if (used) {
			ke.consume(); // otherwise ImageJ would use it!
			return;
		}

		if (input_disabled) {
			if (KeyEvent.VK_ESCAPE == keyCode) {
				// cancel last job if any
				if (Utils.checkYN("Really cancel job?")) {
					display.getProject().getLoader().quitJob(null);
					display.repairGUI();
				}
			}
			ke.consume();
			return; // only zoom is enabled, above
		}

		if (KeyEvent.VK_S == keyCode && 0 == ke.getModifiers() && display.getProject().getLoader().isAsynchronous()) {
			display.getProject().getLoader().saveTask(display.getProject(), "Save");
			ke.consume();
			return;
		} else if (KeyEvent.VK_F == keyCode && Utils.isControlDown(ke)) {
			Search.showWindow();
			ke.consume();
			return;
		}

		// if display is not read-only, check for other keys:
		switch (keyChar) {
		case '<':
		case ',': // select next Layer up
			display.previousLayer(ke.getModifiers()); // repaints as well
			ke.consume();
			return;
		case '>':
		case '.': // select next Layer down
			display.nextLayer(ke.getModifiers());
			ke.consume();
			return;
		}

		if (null == active && null != imp.getRoi() && KeyEvent.VK_A != keyCode) { // control+a and a roi should select under roi
			IJ.getInstance().keyPressed(ke);
			return;
		}

		// end here if display is read-only
		if (display.isReadOnly()) {
			ke.consume();
			display.repaintAll();
			return;
		}

		if (KeyEvent.VK_ENTER == keyCode) {
			if (isTransforming()) {
				applyTransform();
				ke.consume();
				return;
			} else {
				IJ.getInstance().toFront();
				ke.consume();
				return;
			}
		}

		// check preconditions (or the keys are meaningless). Allow 'enter' to
		// bring forward the ImageJ window, and 'v' to paste a patch.
		/*if (null == active && KeyEvent.VK_ENTER != keyCode && KeyEvent.VK_V != keyCode && KeyEvent) {
			return;
		}*/

		final Layer layer = display.getLayer();

		final int mod = ke.getModifiers();

		switch (keyCode) {
			case KeyEvent.VK_COMMA:
			case 0xbc: // select next Layer up
				display.nextLayer(ke.getModifiers());
				break;
			case KeyEvent.VK_PERIOD:
			case 0xbe: // select next Layer down
				display.previousLayer(ke.getModifiers());
				break;
			case KeyEvent.VK_Z:
				// UNDO: shift+z or ctrl+z
				if (0 == (mod ^ Event.SHIFT_MASK) || 0 == (mod ^ Utils.getControlModifier())) {
					Bureaucrat.createAndStart(new Worker.Task("Undo") { @Override
					public void exec() {
						if (isTransforming()) display.getMode().undoOneStep();
						else display.getLayerSet().undoOneStep();
						Display.repaint(display.getLayerSet());
					}}, display.getProject());
					ke.consume();
				// REDO: alt+z or ctrl+shift+z
				} else if (0 == (mod ^ Event.ALT_MASK) || 0 == (mod ^ (Event.SHIFT_MASK | Utils.getControlModifier())) ) {
					Bureaucrat.createAndStart(new Worker.Task("Redo") { @Override
					public void exec() {
						if (isTransforming()) display.getMode().redoOneStep();
						else display.getLayerSet().redoOneStep();
						Display.repaint(display.getLayerSet());
					}}, display.getProject());
					ke.consume();
				}
				// else, the 'z' command restores the image using ImageJ internal undo
				break;
			case KeyEvent.VK_T:
				// Enable with any tool to the left of the PENCIL
				if (null != active && !isTransforming() && ProjectToolbar.getToolId() < ProjectToolbar.PENCIL) {
					ProjectToolbar.setTool(ProjectToolbar.SELECT);
					if (0 == ke.getModifiers()) {
						display.setMode(new AffineTransformMode(display));
					} else if (Event.SHIFT_MASK == ke.getModifiers()) {
						for (final Displayable d : display.getSelection().getSelected()) {
							if (d.isLinked()) {
								Utils.showMessage("Can't enter manual non-linear transformation mode:\nat least one image is linked.");
								return;
							}
						}
						display.setMode(new NonLinearTransformMode(display));
					}
					ke.consume();
				}
				// else, let ImageJ grab the ROI into the Manager, if any
				break;
			case KeyEvent.VK_A:
				if (0 == (ke.getModifiers() ^ Utils.getControlModifier())) {
					final Roi roi = getFakeImagePlus().getRoi();
					if (null != roi) display.getSelection().selectAll(roi, true);
					else display.getSelection().selectAllVisible();
					Display.repaint(display.getLayer(), display.getSelection().getBox(), 0);
					ke.consume();
					break; // INSIDE the 'if' block, so that it can bleed to the default block which forwards to active!
				} else if (null != active) {
					active.keyPressed(ke);
					if (ke.isConsumed()) break;
					// TODO this is just a hack really. Should just fall back to default switch option.
					// The whole keyPressed method needs revision: should not break from it when not using the key.
				}
				break;
			case KeyEvent.VK_ESCAPE: // cancel transformation
				if (isTransforming()) cancelTransform();
				else {
					display.select(null); // deselect
					// repaint out the brush if present
					if (ProjectToolbar.BRUSH == ProjectToolbar.getToolId()) {
						repaint(old_brush_box, 0);
					}
				}
				ke.consume();
				break;
			case KeyEvent.VK_SPACE:
				if (0 == ke.getModifiers()) {
					if (null != active) {
						invalidateVolatile();
						if (Math.abs(active.getAlpha() - 0.5f) > 0.001f) active.setAlpha(0.5f);
						else active.setAlpha(1.0f);
						display.setTransparencySlider(active.getAlpha());
						Display.repaint();
						ke.consume();
					}
				} else {
					// ;)
					final int kem = ke.getModifiers();
					if (0 != (kem & KeyEvent.SHIFT_MASK)
					 && 0 != (kem & KeyEvent.ALT_MASK)
					 && 0 != (kem & KeyEvent.CTRL_MASK)) {
						Utils.showMessage("A mathematician, like a painter or poet,\nis a maker of patterns.\nIf his patterns are more permanent than theirs,\nit is because they are made with ideas\n \nG. H. Hardy.");
						ke.consume();
					 }
				}
				break;
			case KeyEvent.VK_S:
				if (ke.isAltDown()) {
					snapping = true;
					ke.consume();
				} else if (dragging) {
					// ignore improper 's' that open ImageJ's save dialog (linux problem ... in macosx, a single dialog opens with lots of 'ssss...' in the text field)
					ke.consume();
				}
				break;
			case KeyEvent.VK_H:
				handleHide(ke);
				ke.consume();
				break;
			case KeyEvent.VK_J:
				if (!display.getSelection().isEmpty()) {
					display.adjustMinAndMaxGUI();
					ke.consume();
				}
				break;
			case KeyEvent.VK_F1:
			case KeyEvent.VK_F2:
			case KeyEvent.VK_F3:
			case KeyEvent.VK_F4:
			case KeyEvent.VK_F5:
			case KeyEvent.VK_F6:
			case KeyEvent.VK_F7:
			case KeyEvent.VK_F8:
			case KeyEvent.VK_F9:
			case KeyEvent.VK_F10:
			case KeyEvent.VK_F11:
			case KeyEvent.VK_F12:
				ProjectToolbar.keyPressed(ke);
				ke.consume();
				break;
			case KeyEvent.VK_M:
				if (0 == ke.getModifiers() && ProjectToolbar.getToolId() == ProjectToolbar.SELECT) {
					display.getSelection().measure();
					ke.consume();
				}
				break;
		}

		switch (keyChar) {
			case ':':
			case ';':
				if (null != active && active instanceof ZDisplayable) {
					if (null != display.getProject().getProjectTree().tryAddNewConnector(active, true)) {
						ProjectToolbar.setTool(ProjectToolbar.PEN);
					}
					ke.consume();
				}
				break;
		}

		if (ke.isConsumed()) return;

		if (null != active) {
			if (display.getMode().getClass() == DefaultMode.class) {
				active.keyPressed(ke);
			}
			if (ke.isConsumed()) return;
		}

		// Else:
		switch (keyCode) {
			case KeyEvent.VK_G:
				if (browseToNodeLayer(ke.isShiftDown())) {
					ke.consume();
				}
				break;
			case KeyEvent.VK_I:
				if (ke.isAltDown()) {
					if (ke.isShiftDown()) display.importImage();
					else display.importNextImage();
					ke.consume();
				}
				break;
			case KeyEvent.VK_PAGE_UP: // as in Inkscape
				if (null != active) {
					update_graphics = true;
					layer.getParent().addUndoMoveStep(active);
					layer.getParent().move(LayerSet.UP, active);
					layer.getParent().addUndoMoveStep(active);
					Display.repaint(layer, active, 5);
					Display.updatePanelIndex(layer, active);
					ke.consume();
				}
				break;
			case KeyEvent.VK_PAGE_DOWN: // as in Inkscape
				if (null != active) {
					update_graphics = true;
					layer.getParent().addUndoMoveStep(active);
					layer.getParent().move(LayerSet.DOWN, active);
					layer.getParent().addUndoMoveStep(active);
					Display.repaint(layer, active, 5);
					Display.updatePanelIndex(layer, active);
					ke.consume();
				}
				break;
			case KeyEvent.VK_HOME: // as in Inkscape
				if (null != active) {
					update_graphics = true;
					layer.getParent().addUndoMoveStep(active);
					layer.getParent().move(LayerSet.TOP, active);
					layer.getParent().addUndoMoveStep(active);
					Display.repaint(layer, active, 5);
					Display.updatePanelIndex(layer, active);
					ke.consume();
				}
				break;
			case KeyEvent.VK_END: // as in Inkscape
				if (null != active) {
					update_graphics = true;
					layer.getParent().addUndoMoveStep(active);
					layer.getParent().move(LayerSet.BOTTOM, active);
					layer.getParent().addUndoMoveStep(active);
					Display.repaint(layer, active, 5);
					Display.updatePanelIndex(layer, active);
					ke.consume();
				}
				break;
			case KeyEvent.VK_V:
				if (0 == ke.getModifiers()) {
					if (null == active || active.getClass() == Patch.class) {
						// paste a new image
						final ImagePlus clipboard = ImagePlus.getClipboard();
						if (null != clipboard) {
							final ImagePlus imp = new ImagePlus(clipboard.getTitle() + "_" + System.currentTimeMillis(), clipboard.getProcessor().crop());
							final Object info = clipboard.getProperty("Info");
							if (null != info) imp.setProperty("Info", (String)info);
							final double x = srcRect.x + srcRect.width/2 - imp.getWidth()/2;
							final double y = srcRect.y + srcRect.height/2 - imp.getHeight()/2;
							// save the image somewhere:
							final Patch pa = display.getProject().getLoader().addNewImage(imp, x, y);
							display.getLayer().add(pa);
							ke.consume();
						} // TODO there isn't much ImageJ integration in the pasting. Can't paste to a selected image, for example.
					} else {
						// Each type may know how to paste data from the copy buffer into itself:
						active.keyPressed(ke);
						ke.consume();
					}
				}
				break;
			case KeyEvent.VK_P:
				if (0 == ke.getModifiers()) {
					display.getLayerSet().color_cues = !display.getLayerSet().color_cues;
					Display.repaint(display.getLayerSet());
					ke.consume();
				}
				break;
			case KeyEvent.VK_F:
				if (0 == (ke.getModifiers() ^ KeyEvent.SHIFT_MASK)) {
					// toggle visibility of tags
					display.getLayerSet().paint_tags = !display.getLayerSet().paint_tags;
					Display.repaint();
					ke.consume();
				} else if (0 == (ke.getModifiers() ^ KeyEvent.ALT_MASK)) {
					// toggle visibility of edge arrows
					display.getLayerSet().paint_arrows = !display.getLayerSet().paint_arrows;
					Display.repaint();
					ke.consume();
				} else if (0 == ke.getModifiers()) {
					// toggle visibility of edge confidence boxes
					display.getLayerSet().paint_edge_confidence_boxes = !display.getLayerSet().paint_edge_confidence_boxes;
					Display.repaint();
					ke.consume();
				}
				break;
			case KeyEvent.VK_DELETE:
				if (0 == ke.getModifiers()) {
					display.getSelection().deleteAll();
				}
				break;
			case KeyEvent.VK_B:
				if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
					display.duplicateLinkAndSendTo(active, 0, active.getLayer().getParent().previous(layer));
					ke.consume();
				}
				break;
			case KeyEvent.VK_N:
				if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
					display.duplicateLinkAndSendTo(active, 1, active.getLayer().getParent().next(layer));
					ke.consume();
				}
				break;
			case KeyEvent.VK_1:
			case KeyEvent.VK_2:
			case KeyEvent.VK_3:
			case KeyEvent.VK_4:
			case KeyEvent.VK_5:
			case KeyEvent.VK_6:
			case KeyEvent.VK_7:
			case KeyEvent.VK_8:
			case KeyEvent.VK_9:
				// run a plugin, if any
				if (null != Utils.launchTPlugIn(ke, "Display", display.getProject(), display.getActive())) {
					ke.consume();
					break;
				}
		}

		if ( !(keyCode == KeyEvent.VK_UNDEFINED || keyChar == KeyEvent.CHAR_UNDEFINED) && !ke.isConsumed() && null != active && active instanceof Patch) {
			// TODO should allow forwarding for all, not just Patch
			// forward to ImageJ for a final try
			IJ.getInstance().keyPressed(ke);
			repaint(active, 5);
			ke.consume();
		}
		//Utils.log2("keyCode, keyChar: " + keyCode + ", " + keyChar + " ref: " + KeyEvent.VK_UNDEFINED + ", " + KeyEvent.CHAR_UNDEFINED);
	}

	@Override
	public void keyTyped(final KeyEvent ke) {}

	@Override
	public void keyReleased(final KeyEvent ke) {}

	public void zoomToFit() {
		final double magw = (double) getWidth() / imageWidth;
		final double magh = (double) getHeight() / imageHeight;
		this.magnification = magw < magh ? magw : magh;
		this.srcRect.setRect(0, 0, imageWidth, imageHeight);
		setMagnification(magnification);
		display.updateInDatabase("srcRect"); // includes magnification
	}


	public void setReceivesInput(final boolean b) {
		this.input_disabled = !b;
	}

	public boolean isInputEnabled() {
		return !input_disabled;
	}

	/** CAREFUL: the ImageProcessor of the returned ImagePlus is fake, that is, a 4x4 byte array; but the dimensions that it returns are those of the host LayerSet. Used to retrieve ROIs for example.*/
	public ImagePlus getFakeImagePlus() {
		return this.imp;
	}

	/** Key/Mouse bindings like:
	 * - ij.gui.StackWindow: wheel to scroll slices (in this case Layers)
	 * - Inkscape: control+wheel to zoom (apple+wheel in macosx, since control+wheel zooms desktop)
	 */
	@Override
	public void mouseWheelMoved(final MouseWheelEvent mwe) {
		if (dragging) return; // prevent unexpected mouse wheel movements
		final int modifiers = mwe.getModifiers();
		final int rotation = mwe.getWheelRotation();
		final int tool = ProjectToolbar.getToolId();
		if (0 != (modifiers & Utils.getControlModifier())) {
			if (!zoom_and_pan) return;
			// scroll zoom under pointer
			int x = mwe.getX();
			int y = mwe.getY();
			if (x < 0 || y < 0 || x >= getWidth() || y >= getHeight()) {
				x = getWidth()/2;
				y = getHeight()/2;
			}

			// Current mouse point in world coords
			final double xx = x/magnification + srcRect.x;
			final double yy = y/magnification + srcRect.y;
			// Delta of view, in screen pixels:
			final int px_inc;
			if ( 0 != (modifiers & MouseWheelEvent.SHIFT_MASK)) {
				if (0 != (modifiers & MouseWheelEvent.ALT_MASK)) px_inc = 1;
				else px_inc = 5;
			} else px_inc = 20;
			final double inc = px_inc/magnification;

			final Rectangle r = new Rectangle();

			if (rotation > 0) {
				// zoom out
				r.width = srcRect.width + (int)(inc+0.5);
				r.height = srcRect.height + (int)(inc+0.5);
				r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
				r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
				// check boundaries
				if (r.width * magnification < getWidth()
				 || r.height * magnification < getHeight()) {
					// Can't zoom at point: would chage field of view's flow or would have to shift the canvas position!
					Utils.showStatus("To zoom more, use -/+ keys");
					return;
				}
			} else {
				//zoom in
				r.width = srcRect.width - (int)(inc+0.5);
				r.height = srcRect.height - (int)(inc+0.5);
				if (r.width < 1 || r.height < 1) {
					return;
				}
				r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
				r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
			}
			final double newMag = magnification * (srcRect.width / (double)r.width);
			// correct floating-point-induced erroneous drift: the int-precision offscreen point under the mouse shoud remain the same
			r.x -= (int)((x/newMag + r.x) - xx);
			r.y -= (int)((y/newMag + r.y) - yy);

			// adjust bounds
			int w = (int) Math.round(dstWidth / newMag);
			if (w * newMag < dstWidth) w++;
			if (w > imageWidth) w = imageWidth;
			int h = (int) Math.round(dstHeight / newMag);
			if (h * newMag < dstHeight) h++;
			if (h > imageHeight) h = imageHeight;
			if (r.x < 0) r.x = 0;
			if (r.y < 0) r.y = 0;
			if (r.x + w > imageWidth) r.x = imageWidth - w;
			if (r.y + h > imageHeight) r.y = imageHeight - h; //imageWidth and imageHeight are the LayerSet's width,height, ie. the world's 2D dimensions.

			// set!
			this.setMagnification(newMag);
			this.setSrcRect(r.x, r.y, w, h);
			display.repaintAll2();

		} else if (0 == (modifiers ^ InputEvent.SHIFT_MASK) && null != display.getActive() && ProjectToolbar.PEN != tool && AreaContainer.class.isInstance(display.getActive())) {
			final int sign = rotation > 0 ? 1 : -1;
			if (ProjectToolbar.BRUSH == tool) {
				final int brushSize_old = ProjectToolbar.getBrushSize();
				// resize brush for AreaList/AreaTree painting
				int brushSize = ProjectToolbar.setBrushSize((int)(5 * sign / magnification)); // the getWheelRotation provides the sign
				if (brushSize_old > brushSize) brushSize = brushSize_old; // for repainting purposes alone
				int extra = (int)(10 / magnification);
				if (extra < 2) extra = 2;
				extra += 4; // for good measure
				this.repaint(new Rectangle((int)(mwe.getX() / magnification) + srcRect.x - brushSize/2 - extra, (int)(mwe.getY() / magnification) + srcRect.y - brushSize/2 - extra, brushSize+extra, brushSize+extra), 0);
			} else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
				// resize area to consider for fast-marching
				int w = Segmentation.fmp.width;
				int h = Segmentation.fmp.height;
				Segmentation.fmp.resizeArea(sign, magnification);
				w = Math.max(w, Segmentation.fmp.width);
				h = Math.max(h, Segmentation.fmp.height);
				this.repaint(new Rectangle((int)(mwe.getX() / magnification) + srcRect.x - w/2 + 2,
							   (int)(mwe.getY() / magnification) + srcRect.y - h/2 + 2,
							   w + 4, h + 4), 0);
			}
		} else if (0 == modifiers) {
			// scroll layers
			if (rotation > 0) display.nextLayer(modifiers);
			else display.previousLayer(modifiers);
		} else if (null != display.getActive()) {
			// forward to active
			display.getActive().mouseWheelMoved(mwe);
		}
	}

	protected class RepaintProperties implements AbstractOffscreenThread.RepaintProperties {
		final private Layer layer;
		final private List<Layer> layers;
		final private int g_width;
		final private int g_height;
		final private Rectangle srcRect;
		final private double magnification;
		final private Displayable active;
		final private int c_alphas;
		final private Rectangle clipRect;
		final private int mode;
		final private HashMap<Color,Layer> hm;
		final private ArrayList<LayerPanel> blending_list;
		final private GraphicsSource graphics_source;

		RepaintProperties(final Rectangle clipRect, final Layer layer, final List<Layer> layers, final int g_width, final int g_height, final Rectangle srcRect, final double magnification, final Displayable active, final int c_alphas, final GraphicsSource graphics_source) {
			this.clipRect = clipRect;
			this.layer = layer;
			this.layers = layers;
			this.g_width = g_width;
			this.g_height = g_height;
			this.srcRect = srcRect;
			this.magnification = magnification;
			this.active = active;
			this.c_alphas = c_alphas;

			// query the display for repainting mode
			this.hm = new HashMap<Color,Layer>();
			this.blending_list = new ArrayList<LayerPanel>();
			this.mode = display.getPaintMode(hm, blending_list);
			this.graphics_source = graphics_source;
		}
	}

	private final class OffscreenThread extends AbstractOffscreenThread {

		OffscreenThread() {
			super("T2-Canvas-Offscreen");
		}

		@Override
		public void paint() {
			final Layer active_layer;
			final List<Layer> layers;
			final int g_width;
			final int g_height;
			final Rectangle srcRect;
			final double magnification;
			final Displayable active;
			final int c_alphas;
			final Rectangle clipRect;
			final Loader loader;
			final HashMap<Color,Layer> hm;
			final ArrayList<LayerPanel> blending_list;
			final int mode;
			final GraphicsSource graphics_source;

			synchronized (this) {
				final DisplayCanvas.RepaintProperties rp = (DisplayCanvas.RepaintProperties) this.rp;
				active_layer = rp.layer;
				layers = rp.layers;
				g_width = rp.g_width;
				g_height = rp.g_height;
				srcRect = rp.srcRect;
				magnification = rp.magnification;
				active = rp.active;
				c_alphas = rp.c_alphas;
				clipRect = rp.clipRect;
				loader = active_layer.getProject().getLoader();
				mode = rp.mode;
				hm = rp.hm;
				blending_list = rp.blending_list;
				graphics_source = rp.graphics_source;
			}

			BufferedImage target = null;

			final ArrayList<Displayable> al_top = new ArrayList<Displayable>();

			// Check if the image is cached
			Screenshot sc = null;
			try {
				if (display.getMode().getClass() == DefaultMode.class) {
					sc = active_layer.getParent().getScreenshot(new ScreenshotProperties(active_layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source));
					if (null != sc) {
						//Utils.log2("Using cached screenshot " + sc + " with srcRect " + sc.srcRect);
						target = (BufferedImage) loader.getCachedAWT(sc.sid, 0);
						if (null == target) active_layer.getParent().removeFromOffscreens(sc); // the image was thrown out of the cache
						else if ( (sc.al_top.size() > 0 && sc.al_top.get(0) != display.getActive())
						       || (0 == sc.al_top.size() && null != display.getActive()) ) {
							// Can't accept: different active object
							Utils.log2("rejecting: different active object");
							target = null;
						} else {
							al_top.addAll(sc.al_top);
							display.applyFilters(target);
						}
					}
				}
			} catch (final Throwable t) {
				IJError.print(t);
			}

			//Utils.log2("Found target " + target + "\n  with al_top.size() = " + al_top.size());

			if (null == target) {
				target = paintOffscreen(active_layer, layers, g_width, g_height, srcRect, magnification, active, c_alphas, clipRect, loader, hm, blending_list, mode, graphics_source, active_layer.getParent().prepaint, al_top, true);
				// Store it:
				/* CAN'T, may have prePaint in it
				if (null != sc && display.getProject().getProperty("look_ahead_cache", 0) > 0) {
					sc.assoc(target);
					layer.getParent().storeScreenshot(sc);
				}
				*/
			}

			synchronized (offscreen_lock) {
				// only on success:
				if (null != offscreen) to_flush.add(offscreen);
				offscreen = target;
				update_graphics = false;
				DisplayCanvas.this.al_top = al_top;
			}
			// Outside, otherwise could deadlock
			invalidateVolatile();

			// Send repaint event, without offscreen graphics
			RT.paint(clipRect, false);
		}
	}

	/** Looks into the layer and its LayerSet and finds out what needs to be painted, putting it into the three lists.
	 *  @return the index of the first non-image object. */
	private final int gatherDisplayables(final Layer layer, final List<Layer> layers, final Rectangle srcRect, final Displayable active, final ArrayList<Displayable> al_paint, final ArrayList<Displayable> al_top, final boolean preload_patches) {
		layer.getParent().checkBuckets();
		layer.checkBuckets();
		final Iterator<Displayable> ital = layer.find(srcRect, true).iterator();
		final Collection<Displayable> zdal;
		final LayerSet layer_set = layer.getParent();
		// Which layers to color cue, if any?
		if (layer_set.color_cues) {
			final Collection<Displayable> atlayer = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
			final Set<Displayable> others = new HashSet<Displayable>();
			for (final Layer la : layers) {
				if (la == layer) continue;
				others.addAll(layer_set.roughlyFindZDisplayables(la, srcRect, true));
			}
			others.removeAll(atlayer);
			zdal = new ArrayList<Displayable>(others); // in whatever order, to paint under
			zdal.addAll(atlayer); // in proper stack-index order
		} else {
			zdal = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
		}
		final Iterator<Displayable> itzd = zdal.iterator();

		// Assumes the Layer has its objects in order:
		// 1 - Patches
		// 2 - Profiles, Balls
		// 3 - Pipes and ZDisplayables (from the parent LayerSet)
		// 4 - DLabels

		Displayable tmp = null;
		boolean top = false;
		final ArrayList<Patch> al_patches = preload_patches ? new ArrayList<Patch>() : null;

		int first_non_patch = 0;

		while (ital.hasNext()) {
			final Displayable d = ital.next();
			final Class<?> c = d.getClass();
			if (DLabel.class == c || LayerSet.class == c) {
				tmp = d; // since ital.next() has moved forward already
				break;
			}
			if (Patch.class == c) {
				al_paint.add(d);
				if (preload_patches) al_patches.add((Patch)d);
			} else {
				if (!top && d == active) top = true; // no Patch on al_top ever
				if (top) al_top.add(d); // so active is added to al_top, if it's not a Patch
				else al_paint.add(d);
			}
			first_non_patch += 1;
		}

		// preload concurrently as many as possible
		if (preload_patches) Loader.preload(al_patches, magnification, false); // must be false; a 'true' would incur in an infinite loop.

		// paint the ZDisplayables here, before the labels and LayerSets, if any
		while (itzd.hasNext()) {
			final Displayable zd = itzd.next();
			if (zd == active) top = true;
			if (top) al_top.add(zd);
			else al_paint.add(zd);
		}
		// paint LayerSet and DLabel objects!
		if (null != tmp) {
			if (tmp == active) top = true;
			if (top) al_top.add(tmp);
			else al_paint.add(tmp);
		}
		while (ital.hasNext()) {
			final Displayable d = ital.next();
			if (d == active) top = true;
			if (top) al_top.add(d);
			else al_paint.add(d);
		}

		return first_non_patch;
	}

	@Deprecated
	public BufferedImage paintOffscreen(final Layer active_layer, final int g_width, final int g_height,
			final Rectangle srcRect, final double magnification, final Displayable active,
			final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
			final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
			final boolean prepaint, final ArrayList<Displayable> al_top) {
		return paintOffscreen(active_layer, active_layer.getParent().getColorCueLayerRange(active_layer), g_width, g_height, srcRect, magnification, active,
						c_alphas, clipRect, loader, hm, blending_list, mode, graphics_source,
						prepaint, al_top, false);
	}

	/** This method uses data only from the arguments, and changes none.
	 *  Will fill @param al_top with proper Displayable objects, or none when none are selected. */
	public BufferedImage paintOffscreen(final Layer active_layer, final List<Layer> layers, final int g_width, final int g_height,
			final Rectangle srcRect, final double magnification, final Displayable active,
			final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
			final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
			final boolean prepaint, final ArrayList<Displayable> al_top, final boolean preload) {

		final ArrayList<Displayable> al_paint = new ArrayList<Displayable>();
		final int first_non_patch = gatherDisplayables(active_layer, layers, srcRect, active, al_paint, al_top, preload);

		return paintOffscreen(active_layer, layers, al_paint, active, g_width, g_height, c_alphas, loader, hm, blending_list, mode, graphics_source, prepaint, first_non_patch);
	}

	public BufferedImage paintOffscreen(final Layer active_layer, final List<Layer> layers, final ArrayList<Displayable> al_paint, final Displayable active, final int g_width, final int g_height, final int c_alphas, final Loader loader, final HashMap<Color,Layer> hm, final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source, final boolean prepaint, int first_non_patch) {
		try {
			if (0 == g_width || 0 == g_height) return null;
			// ALMOST, but not always perfect //if (null != clipRect) g.setClip(clipRect);

			// prepare the canvas for the srcRect and magnification
			final AffineTransform atc = new AffineTransform();
			atc.scale(magnification, magnification);
			atc.translate(-srcRect.x, -srcRect.y);

			// the non-srcRect areas, in offscreen coords
			final Rectangle r1 = new Rectangle(srcRect.x + srcRect.width, srcRect.y, (int)(g_width / magnification) - srcRect.width, (int)(g_height / magnification));
			final Rectangle r2 = new Rectangle(srcRect.x, srcRect.y + srcRect.height, srcRect.width, (int)(g_height / magnification) - srcRect.height);

			// create new graphics
			try {
				display.getProject().getLoader().releaseToFit(g_width * g_height * 10);
			} catch (final Exception e) {} // when closing, asynch state may throw for a null loader.

			final BufferedImage target = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT); // creates a BufferedImage.TYPE_INT_ARGB image in my T60p ATI FireGL laptop
			//Utils.log2("offscreen acceleration priority: " + target.getAccelerationPriority());
			final Graphics2D g = target.createGraphics();

			g.setTransform(atc); //at_original);

			//setRenderingHints(g);
			// always a stroke of 1.0, regardless of magnification; the stroke below corrects for that
			g.setStroke(stroke);



			// Testing: removed Area.subtract, now need to fill in background
			g.setColor(Color.black);
			g.fillRect(0, 0, g_width - r1.x, g_height - r2.y);


			// paint:
			//  1 - background
			//  2 - images and anything else not on al_top
			//  3 - non-srcRect areas

			//Utils.log2("offscreen painting: " + al_paint.size());

			// filter paintables
			final Collection<? extends Paintable> paintables = graphics_source.asPaintable(al_paint);

			// adjust:
			first_non_patch = paintables.size() - (al_paint.size() - first_non_patch);

			// Determine painting mode
			if (Display.REPAINT_SINGLE_LAYER == mode) {
				if (display.isLiveFilteringEnabled()) {
					paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, true);
				} else {
					// Direct painting mode, with prePaint abilities
					int i = 0;
					for (final Paintable d : paintables) {
						if (i == first_non_patch) {
							//Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
							g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
							//Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
							g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
							//Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
							g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
						}
						if (prepaint) d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
						else d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
						i++;
					}
				}
			} else if (Display.REPAINT_MULTI_LAYER == mode) {
				// paint first the current layer Patches only (to set the background)
				// With prePaint capabilities:
				if (display.isLiveFilteringEnabled()) {
					paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, false);
				} else {
					int i = 0;
					if (prepaint) {
						for (final Paintable d : paintables) {
							if (first_non_patch == i) break;
							d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
							i++;
						}
					} else {
						for (final Paintable d : paintables) {
							if (first_non_patch == i) break;
							d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
							i++;
						}
					}
				}

				// then blend on top the ImageData of the others, in reverse Z order and using the alpha of the LayerPanel
				final Composite original = g.getComposite();
				// reset
				g.setTransform(new AffineTransform());
				// Paint what:
				final Set<Class<?>> included = display.classes_to_multipaint;
				for (final ListIterator<LayerPanel> it = blending_list.listIterator(blending_list.size()); it.hasPrevious(); ) {
					final LayerPanel lp = it.previous();
					if (lp.layer == active_layer) continue;
					active_layer.getProject().getLoader().releaseToFit(g_width * g_height * 4 + 1024);
					final BufferedImage bi = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT);
					final Graphics2D gb = bi.createGraphics();
					gb.setTransform(atc);
					for (final Displayable d : lp.layer.find(srcRect, true)) {
						if (included.contains(d.getClass()))
							d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
					}
					// Repeating loop ... the human compiler at work, just because one cannot lazily concatenate both sequences:
					for (final Displayable d : lp.layer.getParent().roughlyFindZDisplayables(lp.layer, srcRect, true)) {
						if (included.contains(d.getClass()))
								d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
					}
					try {
						g.setComposite(Displayable.getComposite(display.getLayerCompositeMode(lp.layer), lp.getAlpha()));
						g.drawImage(display.applyFilters(bi), 0, 0, null);
					} catch (final Throwable t) {
						Utils.log("Could not use composite mode for layer overlays! Your graphics card may not support it.");
						g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, lp.getAlpha()));
						g.drawImage(bi, 0, 0, null);
						IJError.print(t);
					}
					bi.flush();
				}
				// restore
				g.setComposite(original);
				g.setTransform(atc);

				// then paint the non-Patch objects of the current layer

				//Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
				//Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
				g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
				//Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
				g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

				// TODO this loop should be reading from the paintable_patches and paintables, since their length/order *could* have changed
				// For the current layer:
				for (int i = first_non_patch; i < al_paint.size(); i++) {
					final Displayable d = al_paint.get(i);
					d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
				}
			} else if(Display.REPAINT_RGB_LAYER == mode) {
				// TODO rewrite to avoid calling the list twice
				final Collection<? extends Paintable> paintable_patches = graphics_source.asPaintable(al_paint);
				//
				final HashMap<Color,byte[]> channels = new HashMap<Color,byte[]>();
				hm.put(Color.green, active_layer);
				for (final Map.Entry<Color,Layer> e : hm.entrySet()) {
					final BufferedImage bi = new BufferedImage(g_width, g_height, BufferedImage.TYPE_BYTE_GRAY); //INDEXED, Loader.GRAY_LUT);
					final Graphics2D gb = bi.createGraphics();
					gb.setTransform(atc);
					final Layer la = e.getValue();
					final ArrayList<Paintable> list = new ArrayList<Paintable>();
					if (la == active_layer) {
						if (Color.green != e.getKey()) continue; // don't paint current layer in two channels
						list.addAll(paintable_patches);
					} else {
						list.addAll(la.find(Patch.class, srcRect, true));
					}
					list.addAll(la.getParent().getZDisplayables(ImageData.class, true)); // Stack.class and perhaps others
					for (final Paintable d : list) {
						d.paint(gb, srcRect, magnification, false, c_alphas, la, layers);
					}
					channels.put(e.getKey(), (byte[])new ByteProcessor(bi).getPixels());
				}
				final byte[] red, green, blue;
				green = channels.get(Color.green);
				if (null == channels.get(Color.red)) red = new byte[green.length];
				else red = channels.get(Color.red);
				if (null == channels.get(Color.blue)) blue = new byte[green.length];
				else blue = channels.get(Color.blue);
				final int[] pix = new int[green.length];
				for (int i=0; i<green.length; i++) {
					pix[i] = ((red[i] & 0xff) << 16) + ((green[i] & 0xff) << 8) + (blue[i] & 0xff);
				}
				// undo transform, is intended for Displayable objects
				g.setTransform(new AffineTransform());
				final ColorProcessor cp = new ColorProcessor(g_width, g_height, pix);
				if (display.invert_colors) cp.invert();
				display.applyFilters(cp);
				final Image img = cp.createImage();
				g.drawImage(img, 0, 0, null);
				img.flush();
				// reset
				g.setTransform(atc);

				// then paint the non-Image objects of the current layer
				//Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
				//Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
				g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
				//Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
				g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

				for (final Displayable d : al_paint) {
					if (ImageData.class.isInstance(d)) continue;
					d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
				}
				// TODO having each object type in a key/list<type> table would be so much easier and likely performant.
			}

			// finally, paint non-srcRect areas
			if (r1.width > 0 || r1.height > 0 || r2.width > 0 || r2.height > 0) {
				g.setColor(Color.gray);
				g.setClip(r1);
				g.fill(r1);
				g.setClip(r2);
				g.fill(r2);
			}

			return target;
		} catch (final OutOfMemoryError oome) {
			// so OutOfMemoryError won't generate locks
			IJError.print(oome);
		} catch (final Exception e) {
			IJError.print(e);
		}
		return null;
	}

	private final void paintWithFiltering(final Graphics2D g, final ArrayList<Displayable> al_paint,
										  final Collection<? extends Paintable> paintables,
										  final int first_non_patch,
										  final int g_width, final int g_height,
										  final Displayable active, final int c_alphas,
										  final Layer layer, final List<Layer> layers, final boolean paint_non_images) {
		// Determine the type of the image: if any Patch is of type COLOR_RGB or COLOR_256, use RGB
		int type = BufferedImage.TYPE_BYTE_GRAY;
		search: for (final Displayable d : al_paint) {
			if (d.getClass() == Patch.class) {
				switch (((Patch)d).getType()) {
					case ImagePlus.COLOR_256:
					case ImagePlus.COLOR_RGB:
						type = BufferedImage.TYPE_INT_ARGB;
						break search;
				}
			}
		}

		// Paint all patches to an image
		final BufferedImage bi = new BufferedImage(g_width, g_height, type);
		final Graphics2D gpre = bi.createGraphics();
		gpre.setTransform(atc);
		int i = 0;
		for (final Paintable p : paintables) {
			if (i == first_non_patch) break;
			p.paint(gpre, srcRect, magnification, p == active, c_alphas, layer, layers);
			i++;
		}
		gpre.dispose();
		final ImagePlus imp = new ImagePlus("filtered", type == BufferedImage.TYPE_BYTE_GRAY ? new ByteProcessor(bi) : new ColorProcessor(bi));
		bi.flush();

		display.applyFilters(imp);

		// Paint the filtered image
		final AffineTransform aff = g.getTransform();
		g.setTransform(new AffineTransform()); // reset
		g.drawImage(imp.getProcessor().createImage(), 0, 0, null);
		// Paint the remaining elements if any
		if (paint_non_images && first_non_patch != paintables.size()) {
			g.setTransform(aff); // restore srcRect and magnification
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
			i = 0;
			for (final Paintable p : paintables) {
				if (i < first_non_patch) {
					i++;
					continue;
				}
				p.paint(g, srcRect, magnification, p == active, c_alphas, layer, layers);
				i++;
			}
		}

	}

	// added here to prevent flickering, but doesn't help. All it does is avoid a call to imp.redraw()
	@Override
	protected void scroll(final int sx, final int sy) {
		final int ox = xSrcStart + (int)(sx/magnification);  //convert to offscreen coordinates
		final int oy = ySrcStart + (int)(sy/magnification);
		int newx = xSrcStart + (xMouseStart-ox);
		int newy = ySrcStart + (yMouseStart-oy);
		if (newx<0) newx = 0;
		if (newy<0) newy = 0;
		if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width;
		if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height;
		srcRect.x = newx;
		srcRect.y = newy;
		display.getMode().srcRectUpdated(srcRect, magnification);
	}

	private void handleHide(final KeyEvent ke) {
		if (ke.isAltDown() && !ke.isShiftDown()) {
			// show hidden
			Display.updateCheckboxes(display.getLayer().getParent().setAllVisible(false), DisplayablePanel.VISIBILITY_STATE);
			//Display.repaint(display.getLayer());
			Display.update(display.getLayer());
			ke.consume();
			return;
		}
		if (ke.isShiftDown()) {
			// hide deselected
			display.hideDeselected(ke.isAltDown());
			ke.consume();
			return;
		}
		// else, hide selected
		display.getSelection().setVisible(false);
		Display.update(display.getLayer());
		ke.consume();
	}

	DisplayCanvas.Screenshot createScreenshot(final Layer layer) {
		return new Screenshot(layer);
	}

	protected class ScreenshotProperties {
		final Layer layer;
		final Rectangle srcRect;
		final double magnification;
		final int g_width, g_height, c_alphas;
		final GraphicsSource graphics_source;
		final ArrayList<LayerPanel> blending_list;
		final HashMap<Color,Layer> hm;
		final int mode;
		ScreenshotProperties(final Layer layer, final Rectangle srcRect, final double magnification, final int g_width, final int g_height, final int c_alphas, final GraphicsSource graphics_source) {
			this.srcRect = new Rectangle(srcRect);
			this.magnification = magnification;
			this.layer = layer;
			this.blending_list = new ArrayList<LayerPanel>();
			this.hm = new HashMap<Color,Layer>();
			this.mode = display.getPaintMode(hm, blending_list);
			this.g_width = g_width;
			this.g_height = g_height;
			this.graphics_source = graphics_source;
			this.c_alphas = c_alphas;
			final Layer current_layer = display.getLayer();
			if (Display.REPAINT_RGB_LAYER == mode) {
				final Layer red = hm.get(Color.red);
				final Layer blue = hm.get(Color.blue);
				if (null != red || null != blue) {
					final LayerSet ls = layer.getParent();
					final int i_layer = ls.indexOf(layer);
					final int i_current = ls.indexOf(current_layer);
					if (null != red) {
						final int i_red = ls.indexOf(red);
						final Layer l = red.getParent().getLayer(i_red + i_current - i_layer);
						if (null != l) {
							hm.put(Color.red, l);
						} else {
							hm.remove(Color.red);
						}
					}
					if (null != blue) {
						final int i_blue = ls.indexOf(blue);
						final Layer l = blue.getParent().getLayer(i_blue + i_current - i_layer);
						if (null != l) {
							hm.put(Color.blue, l);
						} else {
							hm.remove(Color.blue);
						}
					}
				}
			}
		}
		@Override
		public final boolean equals(final Object o) {
			final ScreenshotProperties s = (ScreenshotProperties)o;
			return s.layer == this.layer
			  && s.magnification == this.magnification
			  && s.srcRect.x == this.srcRect.x && s.srcRect.y == this.srcRect.y
			  && s.srcRect.width == this.srcRect.width && s.srcRect.height == this.srcRect.height
			  && s.mode == this.mode
			  && s.c_alphas == this.c_alphas
			  && Utils.equalContent(s.blending_list, this.blending_list)
			  && Utils.equalContent(s.hm, this.hm);
		}
		@Override
		public int hashCode() { return 0; } //$%^&$#@!
	}

	public class Screenshot {
		final Layer layer;
		long sid = Long.MIN_VALUE;
		long born = 0;
		final ArrayList<Displayable> al_top = new ArrayList<Displayable>();
		final ScreenshotProperties props;

		Screenshot(final Layer layer) {
			this(layer, DisplayCanvas.this.srcRect, DisplayCanvas.this.magnification, DisplayCanvas.this.getWidth(), DisplayCanvas.this.getHeight(), DisplayCanvas.this.display.getDisplayChannelAlphas(), DisplayCanvas.this.display.getMode().getGraphicsSource());
		}

		Screenshot(final Layer layer, final Rectangle srcRect, final double magnification, final int g_width, final int g_height, final int c_alphas, final GraphicsSource graphics_source) {
			this.layer = layer;
			this.props = new ScreenshotProperties(layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source);
		}

		public long init() {
			this.born = System.currentTimeMillis();
			this.sid = layer.getProject().getLoader().getNextTempId();
			return this.sid;
		}
		/** Associate @param img to this, with a new sid. */
		public long assoc(final BufferedImage img) {
			init();
			if (null != img) layer.getProject().getLoader().cacheAWT(this.sid, img);
			return this.sid;
		}
		public void createImage() {
			final BufferedImage img = paintOffscreen(layer, layer.getParent().getColorCueLayerRange(layer),  props.g_width, props.g_height, props.srcRect, props.magnification,
						  display.getActive(), props.c_alphas, null, layer.getProject().getLoader(),
						  props.hm, props.blending_list, props.mode, props.graphics_source, false, al_top, false);
			layer.getProject().getLoader().cacheAWT(sid, img);
		}
		public void flush() {
			layer.getProject().getLoader().decacheAWT(sid);
		}
	}

	private boolean browseToNodeLayer(final boolean is_shift_down) {
		// find visible instances of Tree that are currently painting in the canvas
		try {
			final Layer active_layer = display.getLayer();
			final Point po = getCursorLoc(); // in offscreen coords
			for (final ZDisplayable zd : display.getLayerSet().getDisplayableList()) {
				if (!zd.isVisible()) continue;
				if (!(zd instanceof Tree<?>)) continue;
				final Tree<?> t = (Tree<?>)zd;
				final Layer la = t.toClosestPaintedNode(active_layer, po.x, po.y, magnification);
				if (null == la) continue;
				// Else:
				display.toLayer(la);
				if (!is_shift_down) display.getSelection().clear();
				display.getSelection().add(t);
				switch (ProjectToolbar.getToolId()) {
					case ProjectToolbar.PEN:
					case ProjectToolbar.BRUSH:
						break;
					default:
						ProjectToolbar.setTool(ProjectToolbar.PEN);
						break;
				}
				return true;
			}
		} catch (final Exception e) {
			Utils.log2("Oops: " + e);
		}
		return false;
	}

	/** Smoothly move the canvas from x0,y0,layer0 to x1,y1,layer1 */
	protected void animateBrowsing(final int dx, final int dy) {
		// check preconditions
		final float mag = (float)this.magnification;
		final Rectangle startSrcRect = (Rectangle)this.srcRect.clone();
		// The motion will be displaced by some screen pixels at every time step.
		final Vector2f v = new Vector2f(dx, dy);
		final float sqdist_to_travel = v.lengthSquared();
		v.normalize();
		v.scale(20/mag);
		final Point2f cp = new Point2f(0, 0); // the current deltas
		//
		final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
		sf[0] = animate(new Runnable() {
			@Override
			public void run() {
				cp.add(v);
				//Utils.log2("advanced by x,y = " + cp.x + ", " + cp.y);
				int x, y;
				if (v.lengthSquared() >= sqdist_to_travel) {
					// set target position
					x = startSrcRect.x + dx;
					y = startSrcRect.y + dy;
					// quit animation
					cancelAnimation(sf[0]);
				} else {
					// set position
					x = startSrcRect.x + (int)(cp.x);
					y = startSrcRect.y + (int)(cp.y);
				}
				setSrcRect(x, y, startSrcRect.width, startSrcRect.height);
				display.repaintAll2();
			}
		}, 0, 50, TimeUnit.MILLISECONDS);
	}

	/** Smoothly move the canvas from its current position until the given rectangle is included within the srcRect.
	 * If the given rectangle is larger than the srcRect, it will refuse to work (for now). */
	public boolean animateBrowsing(final Rectangle target_, final Layer target_layer) {
		// Crop target to world's 2D dimensions
		final Area a = new Area(target_);
		a.intersect(new Area(display.getLayerSet().get2DBounds()));
		final Rectangle target = a.getBounds();
		if (0 == target.width || 0 == target.height) {
			return false;
		}
		// animate at all?
		if (this.srcRect.contains(target) && target_layer == display.getLayer()) {
			// So: don't animate, but at least highlight the target
			playHighlight(target);
			return false;
		}

		// The motion will be displaced by some screen pixels at every time step.
		final int ox = srcRect.x + srcRect.width/2;
		final int oy = srcRect.y + srcRect.height/2;
		final int tx = target.x + target.width/2;
		final int ty = target.y + target.height/2;
		final Vector2f v = new Vector2f(tx - ox, ty - oy);
		v.normalize();
		v.scale(20/(float)magnification);


		// The layer range
		final Layer start_layer = display.getLayer();
		/*
		int ithis = display.getLayerSet().indexOf(start_layer);
		int itarget = display.getLayerSet().indexOf(target_layer);
		final java.util.List<Layer> layers = display.getLayerSet().getLayers(ithis, itarget);
		*/
		final Calibration cal = display.getLayerSet().getCalibrationCopy();
		final double pixelWidth = cal.pixelWidth;
		final double pixelHeight = cal.pixelHeight;

		//final double dist_to_travel = Math.sqrt(Math.pow((tx - ox)*pixelWidth, 2) + Math.pow((ty - oy)*pixelHeight, 2)
		//				                + Math.pow((start_layer.getZ() - target_layer.getZ()) * pixelWidth, 2));

		// vector in calibrated coords between origin and target
		final Vector3d g = new Vector3d((tx - ox)*pixelWidth, (ty - oy)*pixelHeight, (target_layer.getZ() - start_layer.getZ())*pixelWidth);

		final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
		sf[0] = animate(new Runnable() {
			@Override
			public void run() {
				if (DisplayCanvas.this.srcRect.contains(target)) {
					// reached destination
					if (display.getLayer() != target_layer) display.toLayer(target_layer);
					playHighlight(target);
					cancelAnimation(sf[0]);
				} else {
					setSrcRect(srcRect.x + (int)v.x, srcRect.y + (int)v.y, srcRect.width, srcRect.height);
					// which layer?
					if (start_layer != target_layer) {
						final int cx = srcRect.x + srcRect.width/2;
						final int cy = srcRect.y + srcRect.height/2;
						final double dist = Math.sqrt(Math.pow((cx - ox)*pixelWidth, 2) + Math.pow((cy - oy)*pixelHeight, 2)
									+ Math.pow((display.getLayer().getZ() - start_layer.getZ()) * pixelWidth, 2));

						final Vector3d gg = new Vector3d(g);
						gg.normalize();
						gg.scale((float)dist);
						final Layer la = display.getLayerSet().getNearestLayer(start_layer.getZ() + gg.z/pixelWidth);
						if (la != display.getLayer()) {
							display.toLayer(la);
						}
					}
					display.repaintAll2();
				}
			}
		}, 0, 50, TimeUnit.MILLISECONDS);
		return true;
	}

	private ScheduledExecutorService animator = null;
	private boolean zoom_and_pan = true;
	private final Vector<ScheduledFuture<?>> sfs = new Vector<ScheduledFuture<?>>();

	private void cancelAnimations() {
		if (sfs.isEmpty()) return;
		Vector<ScheduledFuture<?>> sfs;
		synchronized (this.sfs) { sfs = new Vector<ScheduledFuture<?>>(this.sfs); }
		for (final ScheduledFuture<?> sf : sfs) {
			sf.cancel(true);
		}
		this.sfs.clear();
		try {
			// wait
			Thread.sleep(150);
		} catch (final InterruptedException ie) {}
		// Re-enable input, in case the watcher task is canceled as well:
		// (It's necessary since there isn't any easy way to tell the scheduler to execute a code block when it cancels its tasks).
		restoreUserInput();
	}
	private void cancelAnimation(final ScheduledFuture<?> sf) {
		sfs.remove(sf);
		sf.cancel(true);
		restoreUserInput();
	}

	private void restoreUserInput() {
		zoom_and_pan = true;
		display.getProject().setReceivesInput(true);
	}

	private ScheduledFuture<?> animate(final Runnable run, final long initialDelay, final long delay, final TimeUnit units) {
		initAnimator();
		// Cancel any animations currently running
		cancelAnimations();
		// Disable user input
		display.getProject().setReceivesInput(false);
		zoom_and_pan = false;
		// Create tasks to run periodically: a task and a watcher task
		final ScheduledFuture<?>[] sf = new ScheduledFuture[2];
		sf[0] = animator.scheduleWithFixedDelay(run, initialDelay, delay, units);
		sf[1] = animator.scheduleWithFixedDelay(new Runnable() {
			@Override
			public void run() {
				if (sf[0].isCancelled()) {
					// Enable user input
					zoom_and_pan = true;
					display.getProject().setReceivesInput(true);
					// cancel yourself
					sf[1].cancel(true);
				}
			}
		}, 100, 700, TimeUnit.MILLISECONDS);
		// Store task for future cancelation
		sfs.add(sf[0]);
		// but not the watcher task, which must finish on its own after the main task finishes.
		return sf[0];
	}

	/** Draw a dotted circle centered on the given Rectangle. */
	private final class Highlighter {
		Ellipse2D.Float elf;
		final Stroke stroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3, new float[]{4,4,4,4}, 0);
		final float dec;
		final Rectangle target;
		Highlighter(final Rectangle target) {
			this.target = target;
			elf = new Ellipse2D.Float(target.x, target.y, target.width, target.height);
			display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
			dec = (float)((Math.max(target.width, target.height)*magnification / 10)/magnification);
		}
		boolean next() {
			invalidateVolatile();
			repaint(target, 5, false);
			// setup next iteration
			display.getLayerSet().getOverlay().remove(elf);
			final Ellipse2D.Float elf2 = (Ellipse2D.Float) elf.clone();
			elf2.x += dec;
			elf2.y += dec;
			elf2.width -= (dec+dec);
			elf2.height -= (dec+dec);
			if (elf2.width > 1 || elf2.height > 1) {
				elf = elf2;
				display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
				return true;
			} else {
				display.getLayerSet().getOverlay().remove(elf);
				return false;
			}
		}
		void cleanup() {
			display.getLayerSet().getOverlay().remove(elf);
		}
	}

	private interface Animation extends Runnable {}

	private ScheduledFuture<?> playHighlight(final Rectangle target) {
		initAnimator();
		final Highlighter highlight = new Highlighter(target);
		final ScheduledFuture<?>[] sf = (ScheduledFuture<?>[])new ScheduledFuture[2];
		sf[0] = animator.scheduleWithFixedDelay(new Animation() {
			@Override
			public void run() {
				if (!highlight.next()) {
					cancelAnimation(sf[0]);
					highlight.cleanup();
				}
			}
		}, 10, 100, TimeUnit.MILLISECONDS);
		sf[1] = animator.scheduleWithFixedDelay(new Animation() {
			@Override
			public void run() {
				if (sf[0].isCancelled()) {
					highlight.cleanup();
					sf[1].cancel(true); // itself
				}
			}
		}, 50, 100, TimeUnit.MILLISECONDS);
		sfs.add(sf[0]);
		return sf[0];
	}

	synchronized private void initAnimator() {
		if (null == animator) animator = Executors.newScheduledThreadPool(2);
	}
}