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