Fix usage of PolygonRoi and other ROIs that depend on ij.gui.ImageCanvas.mouseMoved
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / DisplayCanvas.java
blob4606f9687ac3f70caa82e02097df710549d3dad2
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
25 import java.awt.AWTException;
26 import java.awt.AlphaComposite;
27 import java.awt.BasicStroke;
28 import java.awt.Color;
29 import java.awt.Component;
30 import java.awt.Composite;
31 import java.awt.Cursor;
32 import java.awt.Event;
33 import java.awt.Graphics;
34 import java.awt.Graphics2D;
35 import java.awt.GraphicsConfiguration;
36 import java.awt.Image;
37 import java.awt.Point;
38 import java.awt.Rectangle;
39 import java.awt.RenderingHints;
40 import java.awt.Robot;
41 import java.awt.Stroke;
42 import java.awt.Toolkit;
43 import java.awt.Transparency;
44 import java.awt.event.InputEvent;
45 import java.awt.event.KeyEvent;
46 import java.awt.event.KeyListener;
47 import java.awt.event.MouseEvent;
48 import java.awt.event.MouseWheelEvent;
49 import java.awt.event.MouseWheelListener;
50 import java.awt.geom.AffineTransform;
51 import java.awt.geom.Area;
52 import java.awt.geom.Ellipse2D;
53 import java.awt.image.BufferedImage;
54 import java.awt.image.PixelGrabber;
55 import java.awt.image.VolatileImage;
56 import java.util.ArrayList;
57 import java.util.Collection;
58 import java.util.Collections;
59 import java.util.HashMap;
60 import java.util.HashSet;
61 import java.util.Hashtable;
62 import java.util.Iterator;
63 import java.util.List;
64 import java.util.ListIterator;
65 import java.util.Map;
66 import java.util.Set;
67 import java.util.Vector;
68 import java.util.concurrent.Executors;
69 import java.util.concurrent.ScheduledExecutorService;
70 import java.util.concurrent.ScheduledFuture;
71 import java.util.concurrent.TimeUnit;
73 import org.scijava.vecmath.Point2f;
74 import org.scijava.vecmath.Vector2f;
75 import org.scijava.vecmath.Vector3d;
77 import ij.IJ;
78 import ij.ImagePlus;
79 import ij.Prefs;
80 import ij.WindowManager;
81 import ij.gui.ImageCanvas;
82 import ij.gui.Roi;
83 import ij.gui.Toolbar;
84 import ij.measure.Calibration;
85 import ij.process.ByteProcessor;
86 import ij.process.ColorProcessor;
87 import ini.trakem2.Project;
88 import ini.trakem2.display.graphics.GraphicsSource;
89 import ini.trakem2.display.inspect.InspectPatchTrianglesMode;
90 import ini.trakem2.imaging.Segmentation;
91 import ini.trakem2.persistence.Loader;
92 import ini.trakem2.utils.Bureaucrat;
93 import ini.trakem2.utils.IJError;
94 import ini.trakem2.utils.Lock;
95 import ini.trakem2.utils.ProjectToolbar;
96 import ini.trakem2.utils.Search;
97 import ini.trakem2.utils.Utils;
98 import ini.trakem2.utils.Worker;
100 public final class DisplayCanvas extends ImageCanvas implements KeyListener/*, FocusListener*/, MouseWheelListener {
102 private static final long serialVersionUID = 1L;
104 private Display display;
106 private boolean update_graphics = false;
107 private BufferedImage offscreen = null;
108 private final HashSet<BufferedImage> to_flush = new HashSet<BufferedImage>();
109 private ArrayList<Displayable> al_top = new ArrayList<Displayable>();
111 private final Lock lock_paint = new Lock();
113 private Rectangle box = null; // the bounding box of the active
115 private FakeImageWindow fake_win;
117 private FreeHandProfile freehandProfile = null;
118 private Robot r;// used for setting the mouse pointer
120 private final Object offscreen_lock = new Object();
122 private Cursor noCursor;
124 private boolean snapping = false;
125 private boolean dragging = false;
126 private boolean input_disabled = false;
127 private boolean input_disabled2 = false;
129 /** Store a copy of whatever data as each Class may define it, one such data object per class.
130 * Private to the package. */
131 static private Hashtable<Class<?>,Object> copy_buffer = new Hashtable<Class<?>,Object>();
133 static void setCopyBuffer(final Class<?> c, final Object ob) { copy_buffer.put(c, ob); }
134 static Object getCopyBuffer(final Class<?> c) { return copy_buffer.get(c); }
136 static private boolean openglEnabled = false;
137 static private boolean quartzEnabled = false;
138 static private boolean ddscaleEnabled = false;
140 // Private to the display package:
141 static final RenderingHints rhints;
143 /** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent; */
144 static {
145 final Map<RenderingHints.Key, Object> hints = new HashMap<RenderingHints.Key, Object>();
146 try {
147 final String openglProperty = System.getProperty("sun.java2d.opengl");
148 openglEnabled = openglProperty != null && Boolean.parseBoolean(openglProperty);
149 } catch (final Exception ex) { }
150 try {
151 final String quartzProperty = System.getProperty("apple.awt.graphics.UseQuartz");
152 quartzEnabled = Boolean.parseBoolean(quartzProperty);
153 } catch (final Exception ex) { }
154 try {
155 final String ddscaleProperty = System.getProperty("sun.java2d.ddscale");
156 final String d3dProperty = System.getProperty("sun.java2d.d3d");
157 ddscaleEnabled = Boolean.parseBoolean(ddscaleProperty) && Boolean.parseBoolean(d3dProperty);
158 } catch (final Exception ex) { }
160 if (openglEnabled) {
161 // Bilinear interpolation can be accelerated by the OpenGL pipeline
162 hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
163 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
165 } else if (quartzEnabled) {
166 //hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
167 hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
168 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
169 hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
171 } else if (ddscaleEnabled) {
172 hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
175 rhints = new RenderingHints(hints);
178 private VolatileImage volatileImage;
179 private Object volatile_lock = new Object();
180 //private javax.swing.Timer resourceTimer = new javax.swing.Timer(10000, resourceReaper);
181 //private boolean frameRendered = false;
182 private boolean invalid_volatile = false;
184 /** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent.
185 * MUST be called within a "synchronized (volatile_lock) { ... }" block. */
186 private void renderVolatileImage(final GraphicsConfiguration gc, final BufferedImage offscreen,
187 final ArrayList<Displayable> top, final Displayable active,
188 final Layer active_layer, final List<Layer> layers,
189 final int c_alphas, final AffineTransform at, Rectangle clipRect) {
190 do {
191 // Recreate volatileImage ONLY if necessary: when null, when incompatible, or when dimensions have changed
192 // Otherwise, just paint on top of it
193 final int w = getWidth(), h = getHeight();
194 if (0 == w || 0 == h) return;
195 if (null == volatileImage || volatileImage.getWidth() != w
196 || volatileImage.getHeight() != h || volatileImage.validate(gc) == VolatileImage.IMAGE_INCOMPATIBLE) {
197 if (null != volatileImage) volatileImage.flush();
198 volatileImage = gc.createCompatibleVolatileImage(w, h);
199 volatileImage.setAccelerationPriority(1.0f);
200 invalid_volatile = false;
201 clipRect = null; // paint all
204 // Now paint the BufferedImage into the accelerated image
206 final Graphics2D g = volatileImage.createGraphics();
208 // 0 - set clipRect
209 if (null != clipRect) g.setClip(clipRect);
211 // 1 - Erase any background
212 g.setColor(Color.black);
213 if (null == clipRect) g.fillRect(0, 0, w, h);
214 else g.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
216 // 2 - Paint offscreen image
217 if (null != offscreen) g.drawImage(offscreen, 0, 0, null);
219 // 3 - Paint the active Displayable and all cached on top
221 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
222 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
223 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
224 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
225 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
226 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
228 if (null != active_layer) {
229 g.setTransform(at);
230 g.setStroke(this.stroke); // AFTER setting the transform
231 // Active has to be painted wherever it is, within al_top, if it's not an image
232 //if (null != active && active.getClass() != Patch.class && !active.isOutOfRepaintingClip(magnification, srcRect, clipRect)) active.paint(g, magnification, true, c_alphas, active_layer);
233 final boolean must_paint_active = null != active && active.isVisible() && !ImageData.class.isAssignableFrom(active.getClass());
234 boolean active_painted = !must_paint_active;
236 if (null != top) {
237 final Rectangle tmp = null != clipRect ? new Rectangle() : null;
238 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;
239 for (final Displayable d : top) {
240 if (null != clipRect && !d.getBoundingBox(tmp).intersects(clip)) continue;
241 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
242 if (active_painted) continue;
243 else active_painted = d == active;
246 if (must_paint_active && !active_painted) {
247 // Active may not have been part of top array if it was added new and the offscreen image was not updated,
248 // which is the case for any non-image object
249 // Or, when selecting an object if there were none selected yet.
250 active.paint(g, srcRect, magnification, true, c_alphas, active_layer, layers);
254 display.getMode().getGraphicsSource().paintOnTop(g, display, srcRect, magnification);
256 if (null != active_layer.getOverlay2())
257 active_layer.getOverlay2().paint(g, srcRect, magnification);
258 if (null != active_layer.getParent().getOverlay2())
259 active_layer.getParent().getOverlay2().paint(g, srcRect, magnification);
261 if (null != display.gridoverlay) display.gridoverlay.paint(g);
263 /* // debug: paint the ZDisplayable's bucket in this layer
264 if (null != active_layer.getParent().lbucks) {
265 active_layer.getParent().lbucks.get(active_layer).root.paint(g, srcRect, magnification, Color.red);
269 g.dispose();
270 } while (volatileImage.contentsLost());
273 /** Adapted code from Wayne Meissner, for gstreamer-java org.gstreamer.swing.GstVideoComponent;
274 * Paints (and re-renders, if necessary) the volatile image onto the given Graphics object, which
275 * is that of the DisplayCanvas as provided to the paint(Graphics g) method.
277 * Expects clipRect in screen coordinates
279 private void render(final Graphics g, final Displayable active, final Layer active_layer,
280 final List<Layer> layers, final int c_alphas, final AffineTransform at, Rectangle clipRect) {
281 final Graphics2D g2d = (Graphics2D) g.create();
282 g2d.setRenderingHints(rhints);
283 do {
284 final ArrayList<Displayable> top;
285 final BufferedImage offscreen;
286 synchronized (offscreen_lock) {
287 offscreen = this.offscreen;
288 top = this.al_top; // will never be cleared, but may be swapped
290 final GraphicsConfiguration gc = getGraphicsConfiguration();
291 display.getProject().getLoader().releaseToFit(getWidth() * getHeight() * 4 * 5); // 5 images
293 // Protect volatile image while rendering it
294 synchronized (volatile_lock) {
295 if (invalid_volatile || null == volatileImage
296 || volatileImage.validate(gc) != VolatileImage.IMAGE_OK)
298 // clear clip, remade in full
299 clipRect = null;
300 renderVolatileImage(gc, offscreen, top, active, active_layer, layers, c_alphas, at, clipRect);
302 if (null != clipRect) g2d.setClip(clipRect);
303 g2d.drawImage(volatileImage, 0, 0, null);
305 } while (volatileImage.contentsLost());
307 g2d.dispose();
309 // Flush all old offscreen images
310 synchronized (offscreen_lock) {
311 for (final BufferedImage bi : to_flush) {
312 bi.flush();
314 to_flush.clear();
318 protected void invalidateVolatile() {
319 synchronized (volatile_lock) {
320 this.invalid_volatile = true;
324 /////////////////
326 public DisplayCanvas(final Display display, final int width, final int height) {
327 super(new FakeImagePlus(width, height, display));
328 fake_win = new FakeImageWindow(imp, this, display);
329 this.display = display;
330 this.imageWidth = width;
331 this.imageHeight = height;
332 removeKeyListener(IJ.getInstance());
333 addKeyListener(this);
334 addMouseWheelListener(this);
337 public Display getDisplay() { return display; }
339 /** Used to constrain magnification so that only snapshots are used for painting when opening a new, large and filled Display. */
340 protected void setInitialMagnification(final double mag) { // calling this method 'setMagnification' would conflict with the super class homonimous method.
341 this.magnification = mag; // don't save in the database. This value is overriden when reopening from the database by calling the setup method.
344 /** Used for restoring properties from the database. */
345 public void setup(final double mag, final Rectangle srcRect) {
346 this.magnification = mag;
347 this.srcRect = (Rectangle)srcRect.clone(); // just in case
348 super.setDrawingSize((int)Math.ceil(srcRect.width * mag), (int)Math.ceil(srcRect.height * mag));
349 setMagnification(mag);
350 //no longer needed//display.pack(); // TODO should be run via invokeLater ... need to check many potential locks of invokeLater calling each other.
353 /** Does not repaint. */
354 public void setDimensions(final double width, final double height) {
355 this.imageWidth = (int)Math.ceil(width);
356 this.imageHeight = (int)Math.ceil(height);
357 ((FakeImagePlus)imp).setDimensions(imageWidth, imageHeight);
360 /** Overriding to disable it. */
361 public void handlePopupMenu() {}
363 @Override
364 public final void update(final Graphics g) {
365 // 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.
366 this.paint(g);
369 /** Handles repaint event requests and the generation of offscreen threads. */
370 private final AbstractRepaintThread RT = new AbstractRepaintThread(this, "T2-Canvas-Repainter", new OffscreenThread()) {
371 @Override
372 protected void handleUpdateGraphics(final Component target, final Rectangle clipRect) {
373 final Layer active_layer = display.getLayer();
374 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()));
379 private final void setRenderingHints(final Graphics2D g) {
380 // so slow!! Particularly the first one.
381 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
382 //g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
383 //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
384 //g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
385 //g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
389 @Override
390 public void setMagnification(double mag) {
391 if (mag < 0.00000001) mag = 0.00000001;
392 // ensure a stroke of thickness 1.0 regardless of magnification
393 this.stroke = new BasicStroke((float)(1.0/mag), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
394 // FIXES MAG TO ImageCanvas.zoomLevel LIMITS!!
395 //super.setMagnification(mag);
396 // So, manually:
397 this.magnification = mag;
398 imp.setTitle(imp.getTitle());
399 display.getMode().magnificationUpdated(srcRect, mag);
402 /** Paint lines always with a thickness of 1 pixel. This stroke is modified when the magnification is changed, to compensate. */
403 private BasicStroke stroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
405 /** The affine transform representing the srcRect displacement and the magnification. */
406 private final AffineTransform atc = new AffineTransform();
408 @Override
409 public void paint(final Graphics g) {
410 if (null == g) return;
411 try {
412 synchronized (lock_paint) {
413 lock_paint.lock();
416 // ensure proper positioning
417 g.translate(0, 0); // ints!
419 final Rectangle clipRect = g.getClipBounds();
421 final Displayable active = display.getActive();
422 final int c_alphas = display.getDisplayChannelAlphas();
424 final Layer active_layer = display.getLayer();
425 final List<Layer> layers = active_layer.getParent().getColorCueLayerRange(active_layer);
427 final Graphics2D g2d = (Graphics2D)g;
429 // prepare the canvas for the srcRect and magnification
430 final AffineTransform at_original = g2d.getTransform();
431 atc.setToIdentity();
432 atc.scale(magnification, magnification);
433 atc.translate(-srcRect.x, -srcRect.y);
434 at_original.preConcatenate(atc);
436 if (null != offscreen && dragging) invalidateVolatile(); // to update the active at least
437 render(g, active, active_layer, layers, c_alphas, at_original, clipRect);
439 g2d.setTransform(at_original);
441 g2d.setStroke(this.stroke);
443 // debug buckets
444 //if (null != display.getLayer().root) display.getLayer().root.paint(g2d, srcRect, magnification, Color.red);
445 //if (null != display.getLayer().getParent().lbucks.get(display.getLayer()).root) display.getLayer().getParent().lbucks.get(display.getLayer()).root.paint(g2d, srcRect, magnification, Color.blue);
448 // reset to identity
449 g2d.setTransform(new AffineTransform());
450 // reset to 1.0 thickness
451 g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
453 // paint brush outline for AreaList, or fast-marching area
454 if (mouse_in && null != active && AreaContainer.class.isInstance(active)) {
455 switch (ProjectToolbar.getToolId()) {
456 case ProjectToolbar.BRUSH:
457 final int brushSize = ProjectToolbar.getBrushSize();
458 g.setColor(active.getColor());
459 g.drawOval((int)((xMouse -srcRect.x -brushSize/2)*magnification), (int)((yMouse - srcRect.y -brushSize/2)*magnification), (int)(brushSize * magnification), (int)(brushSize * magnification));
460 break;
461 case ProjectToolbar.PENCIL:
462 case ProjectToolbar.WAND:
463 final Composite co = g2d.getComposite();
464 if (IJ.isWindows()) g2d.setColor(Color.yellow);
465 else g2d.setXORMode(Color.yellow); // XOR on yellow for best contrast
466 g2d.drawRect((int)((xMouse -srcRect.x -Segmentation.fmp.width/2) * magnification),
467 (int)((yMouse -srcRect.y -Segmentation.fmp.height/2) * magnification),
468 (int)(Segmentation.fmp.width * magnification),
469 (int)(Segmentation.fmp.height * magnification));
470 g2d.setComposite(co); // undo XOR mode
471 break;
476 final Roi roi = imp.getRoi();
477 if (null != roi) {
478 roi.draw(g);
481 // Mathias code:
482 if (null != freehandProfile) {
483 freehandProfile.paint(g, magnification, srcRect, true);
484 if(noCursor == null)
485 noCursor = Toolkit.getDefaultToolkit().createCustomCursor(new BufferedImage(1,1,BufferedImage.TYPE_BYTE_BINARY), new Point(0,0), "noCursor");
488 } catch (final Exception e) {
489 Utils.log2("DisplayCanvas.paint(Graphics) Error: " + e);
490 IJError.print(e);
491 } finally {
492 synchronized (lock_paint) {
493 lock_paint.unlock();
498 public void waitForRepaint() {
499 // wait for all offscreen methods to finish painting
500 RT.waitForOffs();
501 // wait for the paint method to finish painting
502 synchronized (lock_paint) {
503 lock_paint.lock();
504 lock_paint.unlock();
508 /** Paints a handle on the screen coords. Adapted from ij.gui.Roi class. */
509 static public void drawHandle(final Graphics g, final int x, final int y, final double magnification) {
510 final int width5 = (int)(5 / magnification + 0.5);
511 final int width3 = (int)(3 / magnification + 0.5);
512 final int corr2 = (int)(2 / magnification + 0.5);
513 final int corr1 = (int)(1 / magnification + 0.5);
514 g.setColor(Color.white);
515 g.drawRect(x - corr2, y - corr2, width5, width5);
516 g.setColor(Color.black);
517 g.drawRect(x - corr1, y - corr1, width3, width3);
518 g.setColor(Color.white);
519 g.fillRect(x, y, corr1, corr1);
522 /** Paints a handle at x,y screen coords. */
523 static public void drawScreenHandle(final Graphics g, final int x, final int y) {
524 g.setColor(Color.orange);
525 g.drawRect(x - 2, y - 2, 5, 5);
526 g.setColor(Color.black);
527 g.drawRect(x - 1, y - 1, 3, 3);
528 g.setColor(Color.orange);
529 g.fillRect(x, y, 1, 1);
532 /** Paints a handle on the offscreen x,y. Adapted from ij.gui.Roi class. */
534 private void drawHandle(Graphics g, double x, double y) {
535 g.setColor(Color.black);
536 g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
537 g.setColor(Color.white);
538 g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);
542 static protected BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
543 static protected AffineTransform DEFAULT_AFFINE = new AffineTransform();
545 static public void drawHandle(final Graphics2D g, final double x, final double y, final Rectangle srcRect, final double magnification) {
546 final AffineTransform original = g.getTransform();
547 g.setTransform(DEFAULT_AFFINE);
548 final Stroke st = g.getStroke();
549 g.setStroke(DEFAULT_STROKE);
551 g.setColor(Color.black);
552 g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
553 g.setColor(Color.white);
554 g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);
556 g.setStroke(st);
557 g.setTransform(original);
560 /** As offscreen. */
561 private int x_p, y_p, x_d, y_d, x_d_old, y_d_old;
563 private boolean popup = false;
565 private boolean locked = false;
567 private int tmp_tool = -1;
569 /** In world coordinates. */
570 protected Point last_popup = null;
572 protected Point consumeLastPopupPoint() {
573 final Point p = last_popup;
574 last_popup = null;
575 return p;
578 @Override
579 public void mousePressed(final MouseEvent me) {
581 super.flags = me.getModifiers();
583 x_p = x_d = srcRect.x + (int) (me.getX() / magnification); // offScreenX(me.getX());
584 y_p = y_d = srcRect.y + (int) (me.getY() / magnification); // offScreenY(me.getY());
586 this.xMouse = x_p;
587 this.yMouse = y_p;
589 // ban if beyond bounds:
590 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
591 return;
594 // Popup:
595 popup = false; // not reset properly in macosx
596 if (Utils.isPopupTrigger(me)) {
597 popup = true;
598 last_popup = new Point(x_p, y_p);
599 display.getPopupMenu().show(this, me.getX(), me.getY());
600 return;
603 // reset
604 snapping = false;
606 int tool = ProjectToolbar.getToolId();
608 // pan with middle mouse like in inkscape
609 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
610 if (0 != (flags & InputEvent.BUTTON2_MASK))
612 if (me.getButton() == MouseEvent.BUTTON2) {
613 tmp_tool = tool;
614 ProjectToolbar.setTool(Toolbar.HAND);
615 tool = Toolbar.HAND;
617 //Utils.log2("button: " + me.getButton() + " BUTTON2: " + MouseEvent.BUTTON2);
619 if (!zoom_and_pan) {
620 // stop animations when clicking (and on pushing ESC)
621 cancelAnimations();
624 switch (tool) {
625 case Toolbar.MAGNIFIER:
626 if (me.isAltDown()) zoomOut(me.getX(), me.getY());
627 else zoomIn(me.getX(), me.getY());
628 return;
629 case Toolbar.HAND:
630 super.setupScroll(x_p, y_p); // offscreen coords.
631 return;
634 if (input_disabled) {
635 input_disabled2 = true;
636 Utils.showMessage("Please wait while completing the task.\nOnly the glass and hand tool are enabled.");
637 return; // only zoom and pan are allowed
640 Displayable active = display.getActive();
642 if (isTransforming() && ProjectToolbar.SELECT != tool) {
643 Utils.logAll("Notice: the 'Select' tool is not active!\n Activate the 'Select' tool to operate transformation modes.");
646 switch (tool) {
647 case ProjectToolbar.PENCIL:
648 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
649 final Profile prof = (Profile) active;
650 this.freehandProfile = new FreeHandProfile(prof);
651 freehandProfile.mousePressed(x_p, y_p);
652 return;
654 break;
655 case Toolbar.RECTANGLE:
656 case Toolbar.OVAL:
657 case Toolbar.POLYGON:
658 case Toolbar.FREEROI:
659 case Toolbar.LINE:
660 case Toolbar.POLYLINE:
661 case Toolbar.FREELINE:
662 case Toolbar.ANGLE:
663 case Toolbar.POINT:
664 // pass the mouse event to superclass ImageCanvas.
665 super.mousePressed(me);
666 repaint();
667 return;
668 case Toolbar.DROPPER:
669 // The color dropper
670 setDrawingColor(x_p, y_p, me.isAltDown());
671 return;
674 // check:
675 if (display.isReadOnly()) return;
677 switch (tool) {
678 case Toolbar.TEXT:
679 if (!isTransforming()) {
680 // edit a label, or add a new one
681 if (null == active || !active.contains(x_p, y_p)) {
682 // find a Displayable to activate, if any
683 display.choose(me.getX(), me.getY(), x_p, y_p, DLabel.class);
684 active = display.getActive();
686 if (null != active && active.isVisible() && active instanceof DLabel) {
687 // edit
688 ((DLabel) active).edit();
689 } else {
690 // new
691 final DLabel label = new DLabel(display.getProject(), " ", x_p, y_p);
692 display.getLayer().add(label);
693 label.edit();
696 return;
699 // SPECIFIC for SELECT and above tools
701 // no ROIs allowed past this point
702 if (tool >= ProjectToolbar.SELECT) imp.killRoi();
703 else return;
705 Selection selection = display.getSelection();
706 if (isTransforming()) {
707 box = display.getMode().getRepaintBounds();
708 display.getMode().mousePressed(me, x_p, y_p, magnification);
709 return;
711 // select or deselect another active Displayable, or add it to the selection group:
712 if (ProjectToolbar.SELECT == tool) {
713 display.choose(me.getX(), me.getY(), x_p, y_p, me.isShiftDown(), null);
715 active = display.getActive();
716 selection = display.getSelection();
718 if (null == active || !active.isVisible()) return;
720 switch (tool) {
721 case ProjectToolbar.SELECT:
722 // gather initial box (for repainting purposes)
723 box = display.getMode().getRepaintBounds();
724 // check if the active is usable:
725 // check if the selection contains locked objects
726 if (selection.isLocked()) {
727 locked = true;
728 return;
730 display.getMode().mousePressed(me, x_p, y_p, magnification);
731 break;
732 default: // the PEN and PENCIL tools, and any other custom tool
733 display.getLayerSet().addPreDataEditStep(active);
734 box = active.getBoundingBox();
735 active.mousePressed(me, display.getLayer(), x_p, y_p, magnification);
736 invalidateVolatile();
737 break;
741 @Override
742 public void mouseDragged(final MouseEvent me) {
744 super.flags = me.getModifiers();
746 if (popup) return;
748 // ban if beyond bounds:
749 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
750 return;
753 if (ProjectToolbar.SELECT == ProjectToolbar.getToolId() && locked) {
754 Utils.log2("Selection is locked.");
755 return;
758 dragging = true;
760 x_d_old = x_d;
761 y_d_old = y_d;
763 x_d = srcRect.x + (int) (me.getX() / magnification); // offscreen
764 y_d = srcRect.y + (int) (me.getY() / magnification);
766 this.xMouse = x_d;
767 this.yMouse = y_d;
769 // protection:
770 final int me_x = me.getX();
771 final int me_y = me.getY();
772 if (me_x < 0 || me_x > this.getWidth() || me_y < 0 || me_y > this.getHeight()) {
773 x_d = x_d_old;
774 y_d = y_d_old;
775 return;
778 final int tool = ProjectToolbar.getToolId();
781 // pan with middle mouse like in inkscape
782 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
783 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
784 tool = Toolbar.HAND;
786 */ // so the above has been implemented as a temporary switch to the HAND tool at the mousePressed function.
788 switch (tool) {
789 case Toolbar.MAGNIFIER: // TODO : create a zooms-area tool
790 return;
791 case Toolbar.HAND:
792 final int srx = srcRect.x,
793 sry = srcRect.y;
794 scroll(me.getX(), me.getY());
795 if (0 != srx - srcRect.x || 0 != sry - srcRect.y) {
796 update_graphics = true; // update the offscreen images.
797 display.getNavigator().repaint(false);
798 repaint(true);
800 return;
803 if (input_disabled2) return;
805 //debug:
806 //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));
808 // Code for Matthias' FreehandProfile (TODO this should be done on mousePressed, not on mouseDragged)
810 final Displayable active = display.getActive();
811 if (null != active && active.getClass() == Profile.class) {
812 try {
813 if (r == null) {
814 r = new Robot(this.getGraphicsConfiguration().getDevice());
816 } catch (final AWTException e) {
817 e.printStackTrace();
821 switch (tool) {
822 case ProjectToolbar.PENCIL:
823 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
824 if (freehandProfile == null)
825 return; // starting painting out of the DisplayCanvas border
826 final double dx = x_d - x_d_old;
827 final double dy = y_d - y_d_old;
828 freehandProfile.mouseDragged(me, x_d, y_d, dx, dy);
829 repaint();
830 // Point screenLocation = getLocationOnScreen();
831 // mousePos[0] += screenLocation.x;
832 // mousePos[1] += screenLocation.y;
833 // r.mouseMove( mousePos[0], mousePos[1]);
834 return;
836 break;
837 case Toolbar.RECTANGLE:
838 case Toolbar.OVAL:
839 case Toolbar.POLYGON:
840 case Toolbar.FREEROI:
841 case Toolbar.LINE:
842 case Toolbar.POLYLINE:
843 case Toolbar.FREELINE:
844 case Toolbar.ANGLE:
845 case Toolbar.POINT:
846 // pass the mouse event to superclass ImageCanvas.
847 super.mouseDragged(me);
848 repaint(false);
849 return;
851 // no ROIs beyond this point
852 if (tool >= ProjectToolbar.SELECT) imp.killRoi();
853 else return;
855 // check:
856 if (display.isReadOnly()) return;
858 if (null != active && active.isVisible()) {
859 // prevent dragging beyond the layer limits
860 if (display.getLayer().contains(x_d, y_d, 1)) {
861 Rectangle box2;
862 switch (tool) {
863 case ProjectToolbar.SELECT:
864 display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
865 box2 = display.getMode().getRepaintBounds();
866 box.add(box2);
867 // repaint all Displays (where it was and where it is now, hence the sum of both boxes):
868 Display.repaint(display.getLayer(), Selection.PADDING, box, false, active.isLinked() || active.getClass() == Patch.class);
869 // box for next mouse dragged iteration
870 box = box2;
871 break;
872 default:
873 active.mouseDragged(me, display.getLayer(), x_p, y_p, x_d, y_d, x_d_old, y_d_old);
874 // the line above must repaint on its own
875 break;
877 } else {
878 //beyond_srcRect = true;
879 Utils.log("DisplayCanvas.mouseDragged: preventing drag beyond layer limits.");
881 } else if (display.getMode() instanceof ManualAlignMode
882 || display.getMode() instanceof InspectPatchTrianglesMode) {
883 if (display.getLayer().contains(x_d, y_d, 1)) {
884 if (tool >= ProjectToolbar.SELECT) {
885 display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
891 @Override
892 public void mouseReleased(final MouseEvent me) {
894 super.flags = me.getModifiers();
896 final boolean dragging2 = dragging;
897 dragging = false;
898 final boolean locked2 = locked;
899 locked = false;
901 if (popup) {
902 popup = false;
903 return;
906 // ban if beyond bounds:
907 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
908 return;
911 final int tool = ProjectToolbar.getToolId();
913 // pan with middle mouse like in inkscape
914 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
915 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
916 tool = Toolbar.HAND;
920 switch (tool) {
921 case Toolbar.MAGNIFIER:
922 // display.updateInDatabase("srcRect"); // TODO if the display.frame
923 // is shrinked, the pack() in the zoom methods will also call the
924 // updateInDatabase("srcRect") (so it's going to be done twice)
925 display.updateFrameTitle();
926 return;
927 case Toolbar.HAND:
928 display.updateInDatabase("srcRect");
929 if (-1 != tmp_tool) {
930 ProjectToolbar.setTool(tmp_tool);
931 tmp_tool = -1;
933 if (!dragging2) repaint(true); // TEMPORARY just to allow fixing bad screen when simply cliking with the hand
934 display.getMode().srcRectUpdated(srcRect, magnification);
935 return;
938 if (input_disabled2) {
939 input_disabled2 = false; // reset
940 return;
943 if (locked2) {
944 if (ProjectToolbar.SELECT == tool) {
945 if (dragging2) {
946 Utils.showMessage("Selection is locked!");
948 return;
952 // pan with middle mouse like in inkscape
953 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
954 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
955 tool = Toolbar.HAND;
959 super.flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set
960 super.flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set
961 super.flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set
963 final int x_r = srcRect.x + (int)(me.getX() / magnification);
964 final int y_r = srcRect.y + (int)(me.getY() / magnification);
967 if (beyond_srcRect) {
968 // Artificial release on the last dragged point
969 x_r = x_d;
970 y_r = y_d;
974 this.xMouse = x_r;
975 this.yMouse = y_r;
977 final Displayable active = display.getActive();
979 switch (tool) {
980 case ProjectToolbar.PENCIL:
981 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
982 if (freehandProfile == null)
983 return; // starting painting out of the DisplayCanvas boarder
984 freehandProfile.mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
985 freehandProfile = null;
986 //repaint(true);
987 final Selection selection = display.getSelection();
988 selection.updateTransform(display.getActive());
989 Display.repaint(display.getLayer(), selection.getBox(), Selection.PADDING); // repaints the navigator as well
990 return;
992 break;
993 case Toolbar.RECTANGLE:
994 case Toolbar.OVAL:
995 case Toolbar.POLYGON:
996 case Toolbar.FREEROI:
997 case Toolbar.LINE:
998 case Toolbar.POLYLINE:
999 case Toolbar.FREELINE:
1000 case Toolbar.ANGLE:
1001 case Toolbar.POINT:
1002 // pass the mouse event to superclass ImageCanvas.
1003 super.mouseReleased(me);
1004 repaint();
1005 // return; // replaced by #SET_ROI
1008 final Roi roi = imp.getRoi();
1010 // check:
1011 if (display.isReadOnly()) return;
1013 if (tool >= ProjectToolbar.SELECT) {
1014 if (null != roi) imp.killRoi();
1015 } else return; // #SET_ROI
1017 final Selection selection = display.getSelection();
1019 if (snapping) {
1020 // finish dragging
1021 display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
1022 box.add(display.getMode().getRepaintBounds());
1023 Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
1024 Display.snap((Patch)active);
1025 // reset:
1026 snapping = false;
1027 return;
1030 if (null != active && active.isVisible()) {
1031 switch(tool) {
1032 case ProjectToolbar.SELECT:
1033 display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
1034 box.add(display.getMode().getRepaintBounds());
1035 Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
1036 break;
1037 case ProjectToolbar.PENCIL:
1038 case ProjectToolbar.PEN:
1039 case ProjectToolbar.BRUSH:
1040 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)
1041 // update active's bounding box
1042 selection.updateTransform(active);
1043 box.add(selection.getBox());
1044 Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
1045 //if (!active.getClass().equals(AreaList.class)) Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
1046 // TODO: this last repaint call is unnecessary, if the box was properly repainted on mouse drag for Profile etc.
1047 //else
1048 if (null != old_brush_box) {
1049 repaint(old_brush_box, 0, false);
1050 old_brush_box = null; // from mouseMoved
1052 // The current state:
1053 display.getLayerSet().addDataEditStep(active);
1054 break;
1059 private boolean mouse_in = false;
1061 @Override
1062 public void mouseEntered(final MouseEvent me) {
1063 mouse_in = true;
1064 // try to catch focus if the JFrame is front most
1065 if (display.isActiveWindow() && !this.hasFocus()) {
1066 this.requestFocus();
1068 // bring dragged point to mouse pointer
1069 // TODO doesn't work as expected.
1071 Displayable active = display.getActive();
1072 int x = offScreenX(me.getX());
1073 int y = offScreenY(me.getY());
1074 if (null != active) {
1075 active.snapTo(x, y, x_p, y_p);
1076 x_p = x_d = x_d_old = x;
1077 y_p = y_d = y_d_old = y;
1080 //Utils.log2("mouseEntered x,y: " + offScreenX(me.getX()) + "," + offScreenY(me.getY()));
1083 @Override
1084 public void mouseExited(final MouseEvent me) {
1085 mouse_in = false;
1086 // paint away the circular brush if any
1087 if (ProjectToolbar.getToolId() == ProjectToolbar.BRUSH) {
1088 final Displayable active = display.getActive();
1089 if (null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
1090 if (null != old_brush_box) {
1091 this.repaint(old_brush_box, 0);
1092 old_brush_box = null;
1098 /** Sets the cursor based on the current tool and cursor location. */
1099 @Override
1100 public void setCursor(final int sx, final int sy, final int ox, final int oy) {
1101 // copy of ImageCanvas.setCursor without the win==null
1102 xMouse = ox;
1103 yMouse = oy;
1104 final Roi roi = imp.getRoi();
1106 * ImageWindow win = imp.getWindow(); if (win==null) return;
1108 if (IJ.spaceBarDown()) {
1109 setCursor(handCursor);
1110 return;
1112 switch (Toolbar.getToolId()) {
1113 case Toolbar.MAGNIFIER:
1114 if (IJ.isMacintosh())
1115 setCursor(defaultCursor);
1116 else
1117 setCursor(moveCursor);
1118 break;
1119 case Toolbar.HAND:
1120 setCursor(handCursor);
1121 break;
1122 case ProjectToolbar.SELECT:
1123 case ProjectToolbar.PENCIL:
1124 setCursor(defaultCursor);
1125 break;
1126 default: // selection tool
1128 if (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.isHandle(sx, sy) >= 0)
1129 setCursor(handCursor);
1130 else if (Prefs.usePointerCursor || (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.contains(ox, oy)))
1131 setCursor(defaultCursor);
1132 else
1133 setCursor(crosshairCursor);
1134 break;
1137 /** Set the srcRect - used by the DisplayNavigator. */
1138 protected void setSrcRect(final int x, final int y, int width, int height) {
1139 if (width < 1) width = 1;
1140 if (height < 1) height = 1;
1141 this.srcRect.setRect(x, y, width, height);
1142 display.updateInDatabase("srcRect");
1143 display.getMode().srcRectUpdated(srcRect, magnification);
1146 @Override
1147 public void setDrawingSize(final int new_width, final int new_height) {
1148 adjustSrcRect(new_width, new_height);
1149 super.setDrawingSize(new_width, new_height);
1152 /** Adjust srcRect and internal variables to the canvas' bounds. */
1153 public void adjustDimensions() {
1154 final Rectangle r = getBounds();
1155 adjustSrcRect(r.width, r.height);
1156 super.dstWidth = r.width;
1157 super.dstHeight = r.height;
1160 /** Adjust srcRect to new dimensions. */
1161 public void adjustSrcRect(final int new_width, final int new_height) {
1162 final double mag = super.getMagnification();
1163 // This method is very important! Make it fit perfectly.
1164 if (srcRect.width * mag < new_width) {
1165 // expand
1166 if (new_width > imageWidth * mag) {
1167 // too large, limit
1168 srcRect.x = 0;
1169 srcRect.width = imageWidth;
1170 } else {
1171 srcRect.width = (int) Math.ceil(new_width / mag);
1172 if (srcRect.x + srcRect.width > imageWidth) {
1173 srcRect.x = imageWidth - srcRect.width;
1176 } else {
1177 // shrink
1178 srcRect.width = (int) Math.ceil(new_width / mag);
1180 if (srcRect.height * mag < new_height) {
1181 // expand
1182 if (new_height > imageHeight * mag) {
1183 // too large, limit
1184 srcRect.y = 0;
1185 srcRect.height = imageHeight;
1186 } else {
1187 srcRect.height = (int) Math.ceil(new_height / mag);
1188 if (srcRect.y + srcRect.height > imageHeight) {
1189 srcRect.y = imageHeight - srcRect.height;
1192 } else {
1193 // shrink
1194 srcRect.height = (int) Math.ceil(new_height / mag);
1198 private void zoomIn2(int x, int y) {
1199 // copy of ImageCanvas.zoomIn except for the canEnlarge is different and
1200 // there's no call to the non-existing ImageWindow
1201 if (magnification >= 32)
1202 return;
1203 final double newMag = getHigherZoomLevel2(magnification);
1205 // zoom at point: correct mag drift
1206 final int cx = getWidth() / 2;
1207 final int cy = getHeight() / 2;
1208 final int dx = (int)(((x - cx) * magnification) / newMag);
1209 final int dy = (int)(((y - cy) * magnification) / newMag);
1210 x -= dx;
1211 y -= dy;
1213 // Adjust the srcRect to the new dimensions
1214 int w = (int) Math.round(dstWidth / newMag);
1215 if (w * newMag < dstWidth)
1216 w++;
1217 if (w > imageWidth)
1218 w = imageWidth;
1219 int h = (int) Math.round(dstHeight / newMag);
1220 if (h * newMag < dstHeight)
1221 h++;
1222 if (h > imageHeight)
1223 h = imageHeight;
1224 x = offScreenX(x);
1225 y = offScreenY(y);
1226 final Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
1227 if (r.x < 0)
1228 r.x = 0;
1229 if (r.y < 0)
1230 r.y = 0;
1231 if (r.x + w > imageWidth)
1232 r.x = imageWidth - w;
1233 if (r.y + h > imageHeight)
1234 r.y = imageHeight - h;
1235 srcRect = r;
1237 //display.pack();
1239 setMagnification(newMag);
1240 display.updateInDatabase("srcRect");
1241 display.repaintAll2(); // this repaint includes this canvas's repaint as well, but also the navigator, etc. // repaint();
1244 private void zoomOut2(int x, int y) {
1245 //if (magnification <= 0.03125)
1246 // return;
1247 final double newMag = getLowerZoomLevel2(magnification);
1249 // zoom at point: correct mag drift
1250 final int cx = getWidth() / 2;
1251 final int cy = getHeight() / 2;
1252 final int dx = (int)(((x - cx) * magnification) / newMag);
1253 final int dy = (int)(((y - cy) * magnification) / newMag);
1254 x -= dx;
1255 y -= dy;
1257 if (imageWidth * newMag > dstWidth || imageHeight * newMag > dstHeight) {
1258 int w = (int) Math.round(dstWidth / newMag);
1259 if (w * newMag < dstWidth)
1260 w++;
1261 int h = (int) Math.round(dstHeight / newMag);
1262 if (h * newMag < dstHeight)
1263 h++;
1264 x = offScreenX(x);
1265 y = offScreenY(y);
1266 final Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
1267 if (r.x < 0)
1268 r.x = 0;
1269 if (r.y < 0)
1270 r.y = 0;
1271 if (r.x + w > imageWidth)
1272 r.x = imageWidth - w;
1273 if (r.y + h > imageHeight)
1274 r.y = imageHeight - h;
1275 srcRect = r;
1276 } else {
1277 // Shrink srcRect, but NOT the dstWidth,dstHeight of the canvas, which remain the same:
1278 srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
1281 setMagnification(newMag);
1282 display.repaintAll2(); // this repaint includes this canvas's repaint, but updates the navigator without update_graphics
1283 display.updateInDatabase("srcRect");
1286 /** The minimum amout of pixels allowed for width or height when zooming out. */
1287 static private final int MIN_DIMENSION = 10; // pixels
1289 /** Enable zooming out up to the point where the display becomes 10 pixels in width or height. */
1290 protected double getLowerZoomLevel2(final double currentMag) {
1291 // if it is 1/72 or lower, then:
1292 if (Math.abs(currentMag - 1/72.0) < 0.00000001 || currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
1293 // find nearest power of two under currentMag
1294 // start at level 7, which is 1/128
1295 int level = 7;
1296 double scale = currentMag;
1297 while (scale * srcRect.width > MIN_DIMENSION && scale * srcRect.height > MIN_DIMENSION) {
1298 scale = 1 / Math.pow(2, level);
1299 // if not equal and actually smaller, break:
1300 if (Math.abs(scale - currentMag) != 0.00000001 && scale < currentMag) break;
1301 level++;
1303 return scale;
1304 } else {
1305 return ImageCanvas.getLowerZoomLevel(currentMag);
1308 protected double getHigherZoomLevel2(final double currentMag) {
1309 // if it is not 1/72 and its lower, then:
1310 if (Math.abs(currentMag - 1/72.0) > 0.00000001 && currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
1311 // find nearest power of two above currentMag
1312 // start at level 14, which is 0.00006103515625 (0.006 %)
1313 int level = 14; // this value may be increased in the future
1314 double scale = currentMag;
1315 while (level >= 0) {
1316 scale = 1 / Math.pow(2, level);
1317 if (scale > currentMag) break;
1318 level--;
1320 return scale;
1321 } else {
1322 return ImageCanvas.getHigherZoomLevel(currentMag);
1328 * // OBSOLETE: modified ij.gui.ImageCanvas directly
1329 public void mouseMoved(MouseEvent e) { if (IJ.getInstance()==null) return; int sx =
1330 * e.getX(); int sy = e.getY(); int ox = offScreenX(sx); int oy =
1331 * offScreenY(sy); flags = e.getModifiers(); setCursor(sx, sy, ox, oy);
1332 * IJ.setInputEvent(e); Roi roi = imp.getRoi(); if (roi!=null &&
1333 * (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE ||
1334 * roi.getType()==Roi.ANGLE) && roi.getState()==roi.CONSTRUCTING) {
1335 * PolygonRoi pRoi = (PolygonRoi)roi; pRoi.handleMouseMove(ox, oy); } else {
1336 * if (ox<imageWidth && oy<imageHeight) { //ImageWindow win =
1337 * imp.getWindow(); //if (win!=null) win.mouseMoved(ox, oy);
1338 * imp.mouseMoved(ox, oy); } else IJ.showStatus(""); } }
1341 private Rectangle old_brush_box = null;
1343 private MouseMovedThread mouse_moved = new MouseMovedThread();
1345 private class MouseMovedThread extends Thread {
1346 private volatile MouseEvent me = null;
1347 MouseMovedThread() {
1348 super("T2-mouseMoved");
1349 setDaemon(true);
1350 setPriority(Thread.NORM_PRIORITY);
1351 start();
1353 void dispatch(final MouseEvent me) {
1354 //Utils.log2("before");
1355 synchronized (this) {
1356 //Utils.log2("in");
1357 this.me = me;
1358 notifyAll();
1361 void quit() {
1362 interrupt();
1363 synchronized (this) { notifyAll(); }
1365 @Override
1366 public void run() {
1367 while (!isInterrupted()) {
1368 MouseEvent me = this.me;
1369 if (null != me) {
1370 try { mouseMoved(me); } catch (final Exception e) { IJError.print(e); }
1372 // Wait only if the event has not changed
1373 synchronized (this) {
1374 if (me == this.me) {
1375 // Release the pointer
1376 me = null;
1377 this.me = null;
1378 if (isInterrupted()) return;
1379 // Wait until there is a new event
1380 try { wait(); } catch (final Exception e) {}
1385 private void mouseMoved(final MouseEvent me) {
1386 if (null == me) return;
1388 if (input_disabled || display.getMode().isDragging()) return;
1390 xMouse = (int)(me.getX() / magnification) + srcRect.x;
1391 yMouse = (int)(me.getY() / magnification) + srcRect.y;
1392 final Displayable active = display.getActive();
1394 // only when no mouse buttons are down
1395 final int flags = DisplayCanvas.super.flags;
1396 if (0 == (flags & InputEvent.BUTTON1_MASK)
1397 /* && 0 == (flags & InputEvent.BUTTON2_MASK) */ // this is the alt key down ..
1398 && 0 == (flags & InputEvent.BUTTON3_MASK)
1399 //if (me.getButton() == MouseEvent.NOBUTTON
1400 && null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
1401 final int tool = ProjectToolbar.getToolId();
1402 Rectangle r = null;
1403 if (ProjectToolbar.BRUSH == tool) {
1404 // repaint area where the brush circle is
1405 final int brushSize = ProjectToolbar.getBrushSize() +2; // +2 padding
1406 r = new Rectangle( xMouse - brushSize/2,
1407 yMouse - brushSize/2,
1408 brushSize+1,
1409 brushSize+1 );
1410 } else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
1411 // repaint area where the fast-marching box is
1412 r = new Rectangle( xMouse - Segmentation.fmp.width/2 - 2,
1413 yMouse - Segmentation.fmp.height/2 - 2,
1414 Segmentation.fmp.width + 4,
1415 Segmentation.fmp.height + 4 );
1417 if (null != r) {
1418 final Rectangle copy = (Rectangle)r.clone();
1419 if (null != old_brush_box) r.add(old_brush_box);
1420 old_brush_box = copy;
1421 repaint(r, 1); // padding because of painting rounding which would live dirty trails
1425 if (me.isShiftDown()) {
1426 // Print a comma-separated list of objects under the mouse pointer
1427 final Layer layer = DisplayCanvas.this.display.getLayer();
1428 final List<Displayable> al = getDisplayablesUnderMouse(me);
1429 if (0 == al.size()) {
1430 Utils.showStatus("", false);
1431 return;
1433 final StringBuilder sb = new StringBuilder();
1434 final Project pr = layer.getProject();
1435 for (final Displayable d : al) sb.append(pr.getShortMeaningfulTitle(d)).append(", ");
1436 sb.setLength(sb.length()-2);
1437 Utils.showStatus(sb.toString(), false);
1438 } else {
1439 // For very large images, the Patch.getPixel can take even half a minute
1440 // to do the pixel grab operation.
1441 //DisplayCanvas.super.mouseMoved(me);
1442 // Instead, find out over what are we
1443 final List<Displayable> under = getDisplayablesUnderMouse(me);
1444 final Calibration cal = display.getLayerSet().getCalibration();
1445 if (under.isEmpty()) {
1446 Utils.showStatus("x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
1447 + ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit());
1448 return;
1450 final Displayable top = under.get(0);
1451 String msg =
1452 "x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
1453 + ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit();
1454 if (top.getClass() == Patch.class) {
1455 final Patch patch = (Patch)top;
1456 final int[] p = new int[4];
1457 BufferedImage offsc;
1458 synchronized (offscreen_lock) {
1459 offsc = offscreen;
1461 if (null == offsc) return;
1462 try {
1463 final PixelGrabber pg = new PixelGrabber(offsc, me.getX(), me.getY(), 1, 1, p, 0, offsc.getWidth(null));
1464 pg.grabPixels();
1465 } catch (final InterruptedException ie) {
1466 IJError.print(ie);
1467 return;
1468 } catch (final Throwable t) {
1469 // The offscreen might have been flushed. Just ignore; pixel value will be reported next.
1470 return;
1472 patch.approximateTransferPixel(p);
1473 msg += ", value=";
1474 switch (patch.getType()) {
1475 case ImagePlus.GRAY16:
1476 case ImagePlus.GRAY8:
1477 msg += p[0];
1478 break;
1479 case ImagePlus.GRAY32:
1480 msg += Float.intBitsToFloat(p[0]);
1481 break;
1482 case ImagePlus.COLOR_RGB:
1483 case ImagePlus.COLOR_256:
1484 msg += "(" + p[0] + "," + p[1] + "," + p[2] + ")";
1485 break;
1487 msg += " [Patch #" + patch.getId() + "]";
1488 } else {
1489 final Color c = top.getColor();
1490 msg += ", value=[" + c.getRed() + "," + c.getGreen() + "," + c.getBlue() + "] [" + Project.getName(top.getClass()) + " #" + top.getId() + "]";
1492 Utils.showStatus(msg);
1497 /** See {@link DisplayCanvas#getDisplayablesUnderMouse(MouseEvent)}. */
1498 public List<Displayable> getDisplayablesUnderMouse() {
1499 return getDisplayablesUnderMouse(new MouseEvent(this, -1, 0, 0, xMouse, yMouse, 1, false));
1502 /** Return the list of Displayable objects under the mouse,
1503 * sorted by proper stack order. */
1504 public List<Displayable> getDisplayablesUnderMouse(final MouseEvent me) {
1505 final Layer layer = display.getLayer();
1506 final int x_p = offScreenX(me.getX()),
1507 y_p = offScreenY(me.getY());
1508 final ArrayList<Displayable> al = new ArrayList<Displayable>(layer.getParent().findZDisplayables(layer, x_p, y_p, true));
1509 Collections.reverse(al);
1510 final ArrayList<Displayable> al2 = new ArrayList<Displayable>(layer.find(x_p, y_p, true));
1511 Collections.reverse(al2);
1512 al.addAll(al2);
1513 return al;
1516 public boolean isDragging() {
1517 return display.getMode().isDragging();
1520 @Override
1521 public void mouseMoved(final MouseEvent me) {
1522 super.flags = me.getModifiers();
1523 final int tool = Toolbar.getToolId();
1524 switch (tool) {
1525 case Toolbar.POLYLINE:
1526 case Toolbar.POLYGON:
1527 case Toolbar.ANGLE:
1528 super.mouseMoved(me);
1529 repaint();
1530 return;
1532 mouse_moved.dispatch(me);
1535 /** Zoom in using the current mouse position, or the center if the mouse is out. */
1536 public void zoomIn() {
1537 if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
1538 zoomIn(dstWidth/2, dstHeight/2);
1539 } else {
1540 zoomIn(screenX(xMouse), screenY(yMouse));
1544 /** Overriding to repaint the DisplayNavigator as well. */
1545 @Override
1546 public void zoomIn(final int x, final int y) {
1547 update_graphics = true; // update the offscreen images.
1548 zoomIn2(x, y);
1551 /** Zoom out using the current mouse position, or the center if the mouse is out. */
1552 public void zoomOut() {
1553 if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
1554 zoomOut(dstWidth/2, dstHeight/2);
1555 } else zoomOut(screenX(xMouse), screenY(yMouse));
1558 /** Overriding to repaint the DisplayNavigator as well. */
1559 @Override
1560 public void zoomOut(final int x, final int y) {
1561 update_graphics = true; // update the offscreen images.
1562 zoomOut2(x, y);
1565 /** Center the srcRect around the given object(s) bounding box, zooming if necessary,
1566 * so that the given r becomes a rectangle centered in the srcRect and zoomed out by a factor of 2. */
1567 public void showCentered(final Rectangle r) {
1568 // multiply bounding box dimensions by two
1569 r.x -= r.width / 2;
1570 r.y -= r.height / 2;
1571 r.width += r.width;
1572 r.height += r.height;
1573 // compute target magnification
1574 final double magn = getWidth() / (double)(r.width > r.height ? r.width : r.height);
1575 center(r, magn);
1578 /** Show the given r as the srcRect (or as much of it as possible) at the given magnification. */
1579 public void center(final Rectangle r, double magn) {
1580 // bring bounds within limits of the layer and the canvas' drawing size
1581 final double lw = display.getLayer().getLayerWidth();
1582 final double lh = display.getLayer().getLayerHeight();
1583 final int cw = (int) (getWidth() / magn); // canvas dimensions in offscreen coords
1584 final int ch = (int) (getHeight() / magn);
1586 // 2nd attempt:
1587 // fit to canvas drawing size:
1588 r.y += (r.height - ch) / 2;
1589 r.width = cw;
1590 r.height = ch;
1591 // place within layer bounds
1592 if (r.x < 0) r.x = 0;
1593 if (r.y < 0) r.y = 0;
1594 if (r.width > lw) {
1595 r.x = 0;
1596 r.width = (int)lw;
1598 if (r.height > lh) {
1599 r.y = 0;
1600 r.height = (int)lh;
1602 if (r.x + r.width > lw) r.x = (int)(lw - cw);
1603 if (r.y + r.height > lh) r.y = (int)(lh - ch);
1604 // compute magn again, since the desired width may have changed:
1605 magn = getWidth() / (double)r.width;
1607 // set magnification and srcRect
1608 setup(magn, r);
1609 try { Thread.sleep(200); } catch (final Exception e) {} // swing ... waiting for the display.pack()
1610 update_graphics = true;
1611 RT.paint(null, update_graphics);
1612 display.updateInDatabase("srcRect");
1613 display.updateFrameTitle();
1614 display.getNavigator().repaint(false);
1617 /** 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. */
1618 public void repaint(final Displayable d) {
1619 repaint(d, 0);
1623 * Repaint as much as the bounding box around the given Displayable plus the
1624 * extra padding. If the Displayable is null, the entire canvas is
1625 * repainted, remaking the offscreen images.
1627 public void repaint(final Displayable displ, final int extra) {
1628 repaint(displ, extra, update_graphics);
1630 public void repaint(final Displayable displ, final int extra, final boolean update_graphics) {
1631 if (null != displ) {
1632 final Rectangle r = displ.getBoundingBox();
1633 r.x = (int) ((r.x - srcRect.x) * magnification) - extra;
1634 r.y = (int) ((r.y - srcRect.y) * magnification) - extra;
1635 r.width = (int) Math.ceil(r.width * magnification) + extra + extra;
1636 r.height = (int) Math.ceil(r.height * magnification) + extra + extra;
1637 invalidateVolatile();
1638 RT.paint(r, update_graphics);
1639 } else {
1640 // everything
1641 repaint(true);
1646 * Repaint the clip corresponding to the sum of all boundingboxes of
1647 * Displayable objects in the hashset.
1649 // it is assumed that the linked objects are close to each other, otherwise
1650 // the clip rectangle grows enormously.
1651 public void repaint(final HashSet<Displayable> hs) {
1652 if (null == hs) return;
1653 Rectangle r = null;
1654 final Layer dl = display.getLayer();
1655 for (final Displayable d : hs) {
1656 if (d.getLayer() == dl) {
1657 if (null == r) r = d.getBoundingBox();
1658 else r.add(d.getBoundingBox());
1661 if (null != r) {
1662 //repaint(r.x, r.y, r.width, r.height);
1663 invalidateVolatile();
1664 RT.paint(r, update_graphics);
1669 * Repaint the given offscreen Rectangle after transforming its data on the fly to the
1670 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
1671 * modified.
1673 public void repaint(final Rectangle r, final int extra) {
1674 invalidateVolatile();
1675 if (null == r) {
1676 //Utils.log2("DisplayCanvas.repaint(Rectangle, int) warning: null r");
1677 RT.paint(null, update_graphics);
1678 return;
1680 // 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);
1681 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);
1685 * Repaint the given Rectangle after transforming its data on the fly to the
1686 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
1687 * modified.
1688 * @param box The rectangle to repaint
1689 * @param extra The extra outbound padding to add to the rectangle
1690 * @param update_graphics Whether to recreate the offscreen images or not
1692 public void repaint(final Rectangle box, final int extra, final boolean update_graphics) {
1693 this.update_graphics = update_graphics;
1694 repaint(box, extra);
1697 /** Repaint everything, updating offscreen graphics if so specified. */
1698 public void repaint(final boolean update_graphics) {
1699 this.update_graphics = update_graphics | this.update_graphics;
1700 invalidateVolatile();
1701 RT.paint(null, this.update_graphics);
1704 /** Overridden to multithread. This method is here basically to enable calls to the FakeImagePlus.draw from the HAND and other tools to repaint properly.*/
1705 @Override
1706 public void repaint() {
1707 //Utils.log2("issuing thread");
1708 invalidateVolatile();
1709 RT.paint(null, update_graphics);
1712 /** Overridden to multithread. */
1713 /* // saved as unoveridden to make sure there are no infinite thread loops when calling super in buggy JVMs
1714 public void repaint(long ms, int x, int y, int width, int height) {
1715 RT.paint(new Rectangle(x, y, width, height), update_graphics);
1719 /** Overridden to multithread. */
1720 @Override
1721 public void repaint(final int x, final int y, final int width, final int height) {
1722 invalidateVolatile();
1723 RT.paint(new Rectangle(x, y, width, height), update_graphics);
1726 public void setUpdateGraphics(final boolean b) {
1727 update_graphics = b;
1730 /** Release offscreen images and stop threads. */
1731 public void flush() {
1732 // cleanup update graphics thread if any
1733 RT.quit();
1734 synchronized (offscreen_lock) {
1735 if (null != offscreen) {
1736 offscreen.flush();
1737 offscreen = null;
1739 update_graphics = true;
1740 for (final BufferedImage bi : to_flush) bi.flush();
1741 to_flush.clear();
1743 mouse_moved.quit();
1744 try {
1745 synchronized (this) { if (null != animator) animator.shutdownNow(); }
1746 cancelAnimations();
1747 } catch (final Exception e) {}
1748 animator = null;
1751 public void destroy() {
1752 flush();
1753 WindowManager.setTempCurrentImage(imp); // the FakeImagePlus
1754 WindowManager.removeWindow(fake_win); // the FakeImageWindow
1757 public boolean applyTransform() {
1758 final boolean b = display.getMode().apply();
1759 if (b) {
1760 display.setMode(new DefaultMode(display));
1761 Display.repaint();
1763 return b;
1766 public boolean isTransforming() {
1767 // TODO this may have to change if modes start getting used for a task other than transformation.
1768 // Perhaps "isTransforming" will have to broaden its meaning to "isNotDefaultMode"
1769 return display.getMode().getClass() != DefaultMode.class;
1772 public void cancelTransform() {
1773 display.getMode().cancel();
1774 display.setMode(new DefaultMode(display));
1775 repaint(true);
1778 private int last_keyCode = KeyEvent.VK_ESCAPE;
1779 private boolean tagging = false;
1781 @Override
1782 public void keyPressed(final KeyEvent ke) {
1784 final Displayable active = display.getActive();
1786 if (null != freehandProfile
1787 && ProjectToolbar.getToolId() == ProjectToolbar.PENCIL
1788 && ke.getKeyCode() == KeyEvent.VK_ESCAPE
1789 && null != freehandProfile)
1791 freehandProfile.abort();
1792 ke.consume();
1793 return;
1796 final int keyCode = ke.getKeyCode();
1798 try {
1799 // Enable tagging system for any alphanumeric key:
1800 if (!input_disabled && null != active && active instanceof Tree<?> && ProjectToolbar.isDataEditTool(ProjectToolbar.getToolId())) {
1801 if (tagging) {
1802 if (KeyEvent.VK_0 == keyCode && KeyEvent.VK_0 != last_keyCode) {
1803 // do nothing: keep tagging as true
1804 } else {
1805 // last step of tagging: a char after t or after t and a number (and the char itself can be a number)
1806 tagging = false;
1808 active.keyPressed(ke);
1809 return;
1810 } else if (KeyEvent.VK_T == keyCode) {
1811 tagging = true;
1812 active.keyPressed(ke);
1813 return;
1816 } finally {
1817 last_keyCode = keyCode;
1820 tagging = false;
1822 if (ke.isConsumed()) return;
1825 * TODO screen editor ... TEMPORARY if (active instanceof DLabel) {
1826 * active.keyPressed(ke); ke.consume(); return; }
1829 if (!zoom_and_pan) {
1830 if (KeyEvent.VK_ESCAPE == keyCode) {
1831 cancelAnimations();
1833 return;
1836 final int keyChar = ke.getKeyChar();
1838 boolean used = false;
1840 switch (keyChar) {
1841 case '+':
1842 case '=':
1843 zoomIn();
1844 used = true;
1845 break;
1846 case '-':
1847 case '_':
1848 zoomOut();
1849 used = true;
1850 break;
1851 default:
1852 break;
1856 if (used) {
1857 ke.consume(); // otherwise ImageJ would use it!
1858 return;
1861 if (input_disabled) {
1862 if (KeyEvent.VK_ESCAPE == keyCode) {
1863 // cancel last job if any
1864 if (Utils.checkYN("Really cancel job?")) {
1865 display.getProject().getLoader().quitJob(null);
1866 display.repairGUI();
1869 ke.consume();
1870 return; // only zoom is enabled, above
1873 if (KeyEvent.VK_S == keyCode && 0 == ke.getModifiers() && display.getProject().getLoader().isAsynchronous()) {
1874 display.getProject().getLoader().saveTask(display.getProject(), "Save");
1875 ke.consume();
1876 return;
1877 } else if (KeyEvent.VK_F == keyCode && Utils.isControlDown(ke)) {
1878 Search.showWindow();
1879 ke.consume();
1880 return;
1883 // if display is not read-only, check for other keys:
1884 switch (keyChar) {
1885 case '<':
1886 case ',': // select next Layer up
1887 display.previousLayer(ke.getModifiers()); // repaints as well
1888 ke.consume();
1889 return;
1890 case '>':
1891 case '.': // select next Layer down
1892 display.nextLayer(ke.getModifiers());
1893 ke.consume();
1894 return;
1897 if (null == active && null != imp.getRoi() && KeyEvent.VK_A != keyCode) { // control+a and a roi should select under roi
1898 IJ.getInstance().keyPressed(ke);
1899 return;
1902 // end here if display is read-only
1903 if (display.isReadOnly()) {
1904 ke.consume();
1905 display.repaintAll();
1906 return;
1909 if (KeyEvent.VK_ENTER == keyCode) {
1910 if (isTransforming()) {
1911 applyTransform();
1912 ke.consume();
1913 return;
1914 } else {
1915 IJ.getInstance().toFront();
1916 ke.consume();
1917 return;
1921 // check preconditions (or the keys are meaningless). Allow 'enter' to
1922 // bring forward the ImageJ window, and 'v' to paste a patch.
1923 /*if (null == active && KeyEvent.VK_ENTER != keyCode && KeyEvent.VK_V != keyCode && KeyEvent) {
1924 return;
1927 final Layer layer = display.getLayer();
1929 final int mod = ke.getModifiers();
1931 switch (keyCode) {
1932 case KeyEvent.VK_COMMA:
1933 case 0xbc: // select next Layer up
1934 display.nextLayer(ke.getModifiers());
1935 break;
1936 case KeyEvent.VK_PERIOD:
1937 case 0xbe: // select next Layer down
1938 display.previousLayer(ke.getModifiers());
1939 break;
1940 case KeyEvent.VK_Z:
1941 // UNDO: shift+z or ctrl+z
1942 if (0 == (mod ^ Event.SHIFT_MASK) || 0 == (mod ^ Utils.getControlModifier())) {
1943 Bureaucrat.createAndStart(new Worker.Task("Undo") { @Override
1944 public void exec() {
1945 if (isTransforming()) display.getMode().undoOneStep();
1946 else display.getLayerSet().undoOneStep();
1947 Display.repaint(display.getLayerSet());
1948 }}, display.getProject());
1949 ke.consume();
1950 // REDO: alt+z or ctrl+shift+z
1951 } else if (0 == (mod ^ Event.ALT_MASK) || 0 == (mod ^ (Event.SHIFT_MASK | Utils.getControlModifier())) ) {
1952 Bureaucrat.createAndStart(new Worker.Task("Redo") { @Override
1953 public void exec() {
1954 if (isTransforming()) display.getMode().redoOneStep();
1955 else display.getLayerSet().redoOneStep();
1956 Display.repaint(display.getLayerSet());
1957 }}, display.getProject());
1958 ke.consume();
1960 // else, the 'z' command restores the image using ImageJ internal undo
1961 break;
1962 case KeyEvent.VK_T:
1963 // Enable with any tool to the left of the PENCIL
1964 if (null != active && !isTransforming() && ProjectToolbar.getToolId() < ProjectToolbar.PENCIL) {
1965 ProjectToolbar.setTool(ProjectToolbar.SELECT);
1966 if (0 == ke.getModifiers()) {
1967 display.setMode(new AffineTransformMode(display));
1968 } else if (Event.SHIFT_MASK == ke.getModifiers()) {
1969 for (final Displayable d : display.getSelection().getSelected()) {
1970 if (d.isLinked()) {
1971 Utils.showMessage("Can't enter manual non-linear transformation mode:\nat least one image is linked.");
1972 return;
1975 display.setMode(new NonLinearTransformMode(display));
1977 ke.consume();
1979 // else, let ImageJ grab the ROI into the Manager, if any
1980 break;
1981 case KeyEvent.VK_A:
1982 if (0 == (ke.getModifiers() ^ Utils.getControlModifier())) {
1983 final Roi roi = getFakeImagePlus().getRoi();
1984 if (null != roi) display.getSelection().selectAll(roi, true);
1985 else display.getSelection().selectAllVisible();
1986 Display.repaint(display.getLayer(), display.getSelection().getBox(), 0);
1987 ke.consume();
1988 break; // INSIDE the 'if' block, so that it can bleed to the default block which forwards to active!
1989 } else if (null != active) {
1990 active.keyPressed(ke);
1991 if (ke.isConsumed()) break;
1992 // TODO this is just a hack really. Should just fall back to default switch option.
1993 // The whole keyPressed method needs revision: should not break from it when not using the key.
1995 break;
1996 case KeyEvent.VK_ESCAPE: // cancel transformation
1997 if (isTransforming()) cancelTransform();
1998 else {
1999 display.select(null); // deselect
2000 // repaint out the brush if present
2001 if (ProjectToolbar.BRUSH == ProjectToolbar.getToolId()) {
2002 repaint(old_brush_box, 0);
2005 ke.consume();
2006 break;
2007 case KeyEvent.VK_SPACE:
2008 if (0 == ke.getModifiers()) {
2009 if (null != active) {
2010 invalidateVolatile();
2011 if (Math.abs(active.getAlpha() - 0.5f) > 0.001f) active.setAlpha(0.5f);
2012 else active.setAlpha(1.0f);
2013 display.setTransparencySlider(active.getAlpha());
2014 Display.repaint();
2015 ke.consume();
2017 } else {
2018 // ;)
2019 final int kem = ke.getModifiers();
2020 if (0 != (kem & KeyEvent.SHIFT_MASK)
2021 && 0 != (kem & KeyEvent.ALT_MASK)
2022 && 0 != (kem & KeyEvent.CTRL_MASK)) {
2023 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.");
2024 ke.consume();
2027 break;
2028 case KeyEvent.VK_S:
2029 if (ke.isAltDown()) {
2030 snapping = true;
2031 ke.consume();
2032 } else if (dragging) {
2033 // 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)
2034 ke.consume();
2036 break;
2037 case KeyEvent.VK_H:
2038 handleHide(ke);
2039 ke.consume();
2040 break;
2041 case KeyEvent.VK_J:
2042 if (!display.getSelection().isEmpty()) {
2043 display.adjustMinAndMaxGUI();
2044 ke.consume();
2046 break;
2047 case KeyEvent.VK_F1:
2048 case KeyEvent.VK_F2:
2049 case KeyEvent.VK_F3:
2050 case KeyEvent.VK_F4:
2051 case KeyEvent.VK_F5:
2052 case KeyEvent.VK_F6:
2053 case KeyEvent.VK_F7:
2054 case KeyEvent.VK_F8:
2055 case KeyEvent.VK_F9:
2056 case KeyEvent.VK_F10:
2057 case KeyEvent.VK_F11:
2058 case KeyEvent.VK_F12:
2059 ProjectToolbar.keyPressed(ke);
2060 ke.consume();
2061 break;
2062 case KeyEvent.VK_M:
2063 if (0 == ke.getModifiers() && ProjectToolbar.getToolId() == ProjectToolbar.SELECT) {
2064 display.getSelection().measure();
2065 ke.consume();
2067 break;
2070 switch (keyChar) {
2071 case ':':
2072 case ';':
2073 if (null != active && active instanceof ZDisplayable) {
2074 if (null != display.getProject().getProjectTree().tryAddNewConnector(active, true)) {
2075 ProjectToolbar.setTool(ProjectToolbar.PEN);
2077 ke.consume();
2079 break;
2082 if (ke.isConsumed()) return;
2084 if (null != active) {
2085 if (display.getMode().getClass() == DefaultMode.class) {
2086 active.keyPressed(ke);
2088 if (ke.isConsumed()) return;
2091 // Else:
2092 switch (keyCode) {
2093 case KeyEvent.VK_G:
2094 if (browseToNodeLayer(ke.isShiftDown())) {
2095 ke.consume();
2097 break;
2098 case KeyEvent.VK_I:
2099 if (ke.isAltDown()) {
2100 if (ke.isShiftDown()) display.importImage();
2101 else display.importNextImage();
2102 ke.consume();
2104 break;
2105 case KeyEvent.VK_PAGE_UP: // as in Inkscape
2106 if (null != active) {
2107 update_graphics = true;
2108 layer.getParent().addUndoMoveStep(active);
2109 layer.getParent().move(LayerSet.UP, active);
2110 layer.getParent().addUndoMoveStep(active);
2111 Display.repaint(layer, active, 5);
2112 Display.updatePanelIndex(layer, active);
2113 ke.consume();
2115 break;
2116 case KeyEvent.VK_PAGE_DOWN: // as in Inkscape
2117 if (null != active) {
2118 update_graphics = true;
2119 layer.getParent().addUndoMoveStep(active);
2120 layer.getParent().move(LayerSet.DOWN, active);
2121 layer.getParent().addUndoMoveStep(active);
2122 Display.repaint(layer, active, 5);
2123 Display.updatePanelIndex(layer, active);
2124 ke.consume();
2126 break;
2127 case KeyEvent.VK_HOME: // as in Inkscape
2128 if (null != active) {
2129 update_graphics = true;
2130 layer.getParent().addUndoMoveStep(active);
2131 layer.getParent().move(LayerSet.TOP, active);
2132 layer.getParent().addUndoMoveStep(active);
2133 Display.repaint(layer, active, 5);
2134 Display.updatePanelIndex(layer, active);
2135 ke.consume();
2137 break;
2138 case KeyEvent.VK_END: // as in Inkscape
2139 if (null != active) {
2140 update_graphics = true;
2141 layer.getParent().addUndoMoveStep(active);
2142 layer.getParent().move(LayerSet.BOTTOM, active);
2143 layer.getParent().addUndoMoveStep(active);
2144 Display.repaint(layer, active, 5);
2145 Display.updatePanelIndex(layer, active);
2146 ke.consume();
2148 break;
2149 case KeyEvent.VK_V:
2150 if (0 == ke.getModifiers()) {
2151 if (null == active || active.getClass() == Patch.class) {
2152 // paste a new image
2153 final ImagePlus clipboard = ImagePlus.getClipboard();
2154 if (null != clipboard) {
2155 final ImagePlus imp = new ImagePlus(clipboard.getTitle() + "_" + System.currentTimeMillis(), clipboard.getProcessor().crop());
2156 final Object info = clipboard.getProperty("Info");
2157 if (null != info) imp.setProperty("Info", (String)info);
2158 final double x = srcRect.x + srcRect.width/2 - imp.getWidth()/2;
2159 final double y = srcRect.y + srcRect.height/2 - imp.getHeight()/2;
2160 // save the image somewhere:
2161 final Patch pa = display.getProject().getLoader().addNewImage(imp, x, y);
2162 display.getLayer().add(pa);
2163 ke.consume();
2164 } // TODO there isn't much ImageJ integration in the pasting. Can't paste to a selected image, for example.
2165 } else {
2166 // Each type may know how to paste data from the copy buffer into itself:
2167 active.keyPressed(ke);
2168 ke.consume();
2171 break;
2172 case KeyEvent.VK_P:
2173 if (0 == ke.getModifiers()) {
2174 display.getLayerSet().color_cues = !display.getLayerSet().color_cues;
2175 Display.repaint(display.getLayerSet());
2176 ke.consume();
2178 break;
2179 case KeyEvent.VK_F:
2180 if (0 == (ke.getModifiers() ^ KeyEvent.SHIFT_MASK)) {
2181 // toggle visibility of tags
2182 display.getLayerSet().paint_tags = !display.getLayerSet().paint_tags;
2183 Display.repaint();
2184 ke.consume();
2185 } else if (0 == (ke.getModifiers() ^ KeyEvent.ALT_MASK)) {
2186 // toggle visibility of edge arrows
2187 display.getLayerSet().paint_arrows = !display.getLayerSet().paint_arrows;
2188 Display.repaint();
2189 ke.consume();
2190 } else if (0 == ke.getModifiers()) {
2191 // toggle visibility of edge confidence boxes
2192 display.getLayerSet().paint_edge_confidence_boxes = !display.getLayerSet().paint_edge_confidence_boxes;
2193 Display.repaint();
2194 ke.consume();
2196 break;
2197 case KeyEvent.VK_DELETE:
2198 if (0 == ke.getModifiers()) {
2199 display.getSelection().deleteAll();
2201 break;
2202 case KeyEvent.VK_B:
2203 if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
2204 display.duplicateLinkAndSendTo(active, 0, active.getLayer().getParent().previous(layer));
2205 ke.consume();
2207 break;
2208 case KeyEvent.VK_N:
2209 if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
2210 display.duplicateLinkAndSendTo(active, 1, active.getLayer().getParent().next(layer));
2211 ke.consume();
2213 break;
2214 case KeyEvent.VK_1:
2215 case KeyEvent.VK_2:
2216 case KeyEvent.VK_3:
2217 case KeyEvent.VK_4:
2218 case KeyEvent.VK_5:
2219 case KeyEvent.VK_6:
2220 case KeyEvent.VK_7:
2221 case KeyEvent.VK_8:
2222 case KeyEvent.VK_9:
2223 // run a plugin, if any
2224 if (null != Utils.launchTPlugIn(ke, "Display", display.getProject(), display.getActive())) {
2225 ke.consume();
2226 break;
2230 if ( !(keyCode == KeyEvent.VK_UNDEFINED || keyChar == KeyEvent.CHAR_UNDEFINED) && !ke.isConsumed() && null != active && active instanceof Patch) {
2231 // TODO should allow forwarding for all, not just Patch
2232 // forward to ImageJ for a final try
2233 IJ.getInstance().keyPressed(ke);
2234 repaint(active, 5);
2235 ke.consume();
2237 //Utils.log2("keyCode, keyChar: " + keyCode + ", " + keyChar + " ref: " + KeyEvent.VK_UNDEFINED + ", " + KeyEvent.CHAR_UNDEFINED);
2240 @Override
2241 public void keyTyped(final KeyEvent ke) {}
2243 @Override
2244 public void keyReleased(final KeyEvent ke) {}
2246 public void zoomToFit() {
2247 final double magw = (double) getWidth() / imageWidth;
2248 final double magh = (double) getHeight() / imageHeight;
2249 this.magnification = magw < magh ? magw : magh;
2250 this.srcRect.setRect(0, 0, imageWidth, imageHeight);
2251 setMagnification(magnification);
2252 display.updateInDatabase("srcRect"); // includes magnification
2256 public void setReceivesInput(final boolean b) {
2257 this.input_disabled = !b;
2260 public boolean isInputEnabled() {
2261 return !input_disabled;
2264 /** 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.*/
2265 public ImagePlus getFakeImagePlus() {
2266 return this.imp;
2269 /** Key/Mouse bindings like:
2270 * - ij.gui.StackWindow: wheel to scroll slices (in this case Layers)
2271 * - Inkscape: control+wheel to zoom (apple+wheel in macosx, since control+wheel zooms desktop)
2273 @Override
2274 public void mouseWheelMoved(final MouseWheelEvent mwe) {
2275 if (dragging) return; // prevent unexpected mouse wheel movements
2276 final int modifiers = mwe.getModifiers();
2277 final int rotation = mwe.getWheelRotation();
2278 final int tool = ProjectToolbar.getToolId();
2279 if (0 != (modifiers & Utils.getControlModifier())) {
2280 if (!zoom_and_pan) return;
2281 // scroll zoom under pointer
2282 int x = mwe.getX();
2283 int y = mwe.getY();
2284 if (x < 0 || y < 0 || x >= getWidth() || y >= getHeight()) {
2285 x = getWidth()/2;
2286 y = getHeight()/2;
2289 // Current mouse point in world coords
2290 final double xx = x/magnification + srcRect.x;
2291 final double yy = y/magnification + srcRect.y;
2292 // Delta of view, in screen pixels:
2293 final int px_inc;
2294 if ( 0 != (modifiers & MouseWheelEvent.SHIFT_MASK)) {
2295 if (0 != (modifiers & MouseWheelEvent.ALT_MASK)) px_inc = 1;
2296 else px_inc = 5;
2297 } else px_inc = 20;
2298 final double inc = px_inc/magnification;
2300 final Rectangle r = new Rectangle();
2302 if (rotation > 0) {
2303 // zoom out
2304 r.width = srcRect.width + (int)(inc+0.5);
2305 r.height = srcRect.height + (int)(inc+0.5);
2306 r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
2307 r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
2308 // check boundaries
2309 if (r.width * magnification < getWidth()
2310 || r.height * magnification < getHeight()) {
2311 // Can't zoom at point: would chage field of view's flow or would have to shift the canvas position!
2312 Utils.showStatus("To zoom more, use -/+ keys");
2313 return;
2315 } else {
2316 //zoom in
2317 r.width = srcRect.width - (int)(inc+0.5);
2318 r.height = srcRect.height - (int)(inc+0.5);
2319 if (r.width < 1 || r.height < 1) {
2320 return;
2322 r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
2323 r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
2325 final double newMag = magnification * (srcRect.width / (double)r.width);
2326 // correct floating-point-induced erroneous drift: the int-precision offscreen point under the mouse shoud remain the same
2327 r.x -= (int)((x/newMag + r.x) - xx);
2328 r.y -= (int)((y/newMag + r.y) - yy);
2330 // adjust bounds
2331 int w = (int) Math.round(dstWidth / newMag);
2332 if (w * newMag < dstWidth) w++;
2333 if (w > imageWidth) w = imageWidth;
2334 int h = (int) Math.round(dstHeight / newMag);
2335 if (h * newMag < dstHeight) h++;
2336 if (h > imageHeight) h = imageHeight;
2337 if (r.x < 0) r.x = 0;
2338 if (r.y < 0) r.y = 0;
2339 if (r.x + w > imageWidth) r.x = imageWidth - w;
2340 if (r.y + h > imageHeight) r.y = imageHeight - h; //imageWidth and imageHeight are the LayerSet's width,height, ie. the world's 2D dimensions.
2342 // set!
2343 this.setMagnification(newMag);
2344 this.setSrcRect(r.x, r.y, w, h);
2345 display.repaintAll2();
2347 } else if (0 == (modifiers ^ InputEvent.SHIFT_MASK) && null != display.getActive() && ProjectToolbar.PEN != tool && AreaContainer.class.isInstance(display.getActive())) {
2348 final int sign = rotation > 0 ? 1 : -1;
2349 if (ProjectToolbar.BRUSH == tool) {
2350 final int brushSize_old = ProjectToolbar.getBrushSize();
2351 // resize brush for AreaList/AreaTree painting
2352 int brushSize = ProjectToolbar.setBrushSize((int)(5 * sign / magnification)); // the getWheelRotation provides the sign
2353 if (brushSize_old > brushSize) brushSize = brushSize_old; // for repainting purposes alone
2354 int extra = (int)(10 / magnification);
2355 if (extra < 2) extra = 2;
2356 extra += 4; // for good measure
2357 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);
2358 } else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
2359 // resize area to consider for fast-marching
2360 int w = Segmentation.fmp.width;
2361 int h = Segmentation.fmp.height;
2362 Segmentation.fmp.resizeArea(sign, magnification);
2363 w = Math.max(w, Segmentation.fmp.width);
2364 h = Math.max(h, Segmentation.fmp.height);
2365 this.repaint(new Rectangle((int)(mwe.getX() / magnification) + srcRect.x - w/2 + 2,
2366 (int)(mwe.getY() / magnification) + srcRect.y - h/2 + 2,
2367 w + 4, h + 4), 0);
2369 } else if (0 == modifiers) {
2370 // scroll layers
2371 if (rotation > 0) display.nextLayer(modifiers);
2372 else display.previousLayer(modifiers);
2373 } else if (null != display.getActive()) {
2374 // forward to active
2375 display.getActive().mouseWheelMoved(mwe);
2379 protected class RepaintProperties implements AbstractOffscreenThread.RepaintProperties {
2380 final private Layer layer;
2381 final private List<Layer> layers;
2382 final private int g_width;
2383 final private int g_height;
2384 final private Rectangle srcRect;
2385 final private double magnification;
2386 final private Displayable active;
2387 final private int c_alphas;
2388 final private Rectangle clipRect;
2389 final private int mode;
2390 final private HashMap<Color,Layer> hm;
2391 final private ArrayList<LayerPanel> blending_list;
2392 final private GraphicsSource graphics_source;
2394 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) {
2395 this.clipRect = clipRect;
2396 this.layer = layer;
2397 this.layers = layers;
2398 this.g_width = g_width;
2399 this.g_height = g_height;
2400 this.srcRect = srcRect;
2401 this.magnification = magnification;
2402 this.active = active;
2403 this.c_alphas = c_alphas;
2405 // query the display for repainting mode
2406 this.hm = new HashMap<Color,Layer>();
2407 this.blending_list = new ArrayList<LayerPanel>();
2408 this.mode = display.getPaintMode(hm, blending_list);
2409 this.graphics_source = graphics_source;
2413 private final class OffscreenThread extends AbstractOffscreenThread {
2415 OffscreenThread() {
2416 super("T2-Canvas-Offscreen");
2419 @Override
2420 public void paint() {
2421 final Layer active_layer;
2422 final List<Layer> layers;
2423 final int g_width;
2424 final int g_height;
2425 final Rectangle srcRect;
2426 final double magnification;
2427 final Displayable active;
2428 final int c_alphas;
2429 final Rectangle clipRect;
2430 final Loader loader;
2431 final HashMap<Color,Layer> hm;
2432 final ArrayList<LayerPanel> blending_list;
2433 final int mode;
2434 final GraphicsSource graphics_source;
2436 synchronized (this) {
2437 final DisplayCanvas.RepaintProperties rp = (DisplayCanvas.RepaintProperties) this.rp;
2438 active_layer = rp.layer;
2439 layers = rp.layers;
2440 g_width = rp.g_width;
2441 g_height = rp.g_height;
2442 srcRect = rp.srcRect;
2443 magnification = rp.magnification;
2444 active = rp.active;
2445 c_alphas = rp.c_alphas;
2446 clipRect = rp.clipRect;
2447 loader = active_layer.getProject().getLoader();
2448 mode = rp.mode;
2449 hm = rp.hm;
2450 blending_list = rp.blending_list;
2451 graphics_source = rp.graphics_source;
2454 BufferedImage target = null;
2456 final ArrayList<Displayable> al_top = new ArrayList<Displayable>();
2458 // Check if the image is cached
2459 Screenshot sc = null;
2460 try {
2461 if (display.getMode().getClass() == DefaultMode.class) {
2462 sc = active_layer.getParent().getScreenshot(new ScreenshotProperties(active_layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source));
2463 if (null != sc) {
2464 //Utils.log2("Using cached screenshot " + sc + " with srcRect " + sc.srcRect);
2465 target = (BufferedImage) loader.getCachedAWT(sc.sid, 0);
2466 if (null == target) active_layer.getParent().removeFromOffscreens(sc); // the image was thrown out of the cache
2467 else if ( (sc.al_top.size() > 0 && sc.al_top.get(0) != display.getActive())
2468 || (0 == sc.al_top.size() && null != display.getActive()) ) {
2469 // Can't accept: different active object
2470 Utils.log2("rejecting: different active object");
2471 target = null;
2472 } else {
2473 al_top.addAll(sc.al_top);
2474 display.applyFilters(target);
2478 } catch (final Throwable t) {
2479 IJError.print(t);
2482 //Utils.log2("Found target " + target + "\n with al_top.size() = " + al_top.size());
2484 if (null == target) {
2485 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);
2486 // Store it:
2487 /* CAN'T, may have prePaint in it
2488 if (null != sc && display.getProject().getProperty("look_ahead_cache", 0) > 0) {
2489 sc.assoc(target);
2490 layer.getParent().storeScreenshot(sc);
2495 synchronized (offscreen_lock) {
2496 // only on success:
2497 if (null != offscreen) to_flush.add(offscreen);
2498 offscreen = target;
2499 update_graphics = false;
2500 DisplayCanvas.this.al_top = al_top;
2502 // Outside, otherwise could deadlock
2503 invalidateVolatile();
2505 // Send repaint event, without offscreen graphics
2506 RT.paint(clipRect, false);
2510 /** Looks into the layer and its LayerSet and finds out what needs to be painted, putting it into the three lists.
2511 * @return the index of the first non-image object. */
2512 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) {
2513 layer.getParent().checkBuckets();
2514 layer.checkBuckets();
2515 final Iterator<Displayable> ital = layer.find(srcRect, true).iterator();
2516 final Collection<Displayable> zdal;
2517 final LayerSet layer_set = layer.getParent();
2518 // Which layers to color cue, if any?
2519 if (layer_set.color_cues) {
2520 final Collection<Displayable> atlayer = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
2521 final Set<Displayable> others = new HashSet<Displayable>();
2522 for (final Layer la : layers) {
2523 if (la == layer) continue;
2524 others.addAll(layer_set.roughlyFindZDisplayables(la, srcRect, true));
2526 others.removeAll(atlayer);
2527 zdal = new ArrayList<Displayable>(others); // in whatever order, to paint under
2528 zdal.addAll(atlayer); // in proper stack-index order
2529 } else {
2530 zdal = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
2532 final Iterator<Displayable> itzd = zdal.iterator();
2534 // Assumes the Layer has its objects in order:
2535 // 1 - Patches
2536 // 2 - Profiles, Balls
2537 // 3 - Pipes and ZDisplayables (from the parent LayerSet)
2538 // 4 - DLabels
2540 Displayable tmp = null;
2541 boolean top = false;
2542 final ArrayList<Patch> al_patches = preload_patches ? new ArrayList<Patch>() : null;
2544 int first_non_patch = 0;
2546 while (ital.hasNext()) {
2547 final Displayable d = ital.next();
2548 final Class<?> c = d.getClass();
2549 if (DLabel.class == c || LayerSet.class == c) {
2550 tmp = d; // since ital.next() has moved forward already
2551 break;
2553 if (Patch.class == c) {
2554 al_paint.add(d);
2555 if (preload_patches) al_patches.add((Patch)d);
2556 } else {
2557 if (!top && d == active) top = true; // no Patch on al_top ever
2558 if (top) al_top.add(d); // so active is added to al_top, if it's not a Patch
2559 else al_paint.add(d);
2561 first_non_patch += 1;
2564 // preload concurrently as many as possible
2565 if (preload_patches) Loader.preload(al_patches, magnification, false); // must be false; a 'true' would incur in an infinite loop.
2567 // paint the ZDisplayables here, before the labels and LayerSets, if any
2568 while (itzd.hasNext()) {
2569 final Displayable zd = itzd.next();
2570 if (zd == active) top = true;
2571 if (top) al_top.add(zd);
2572 else al_paint.add(zd);
2574 // paint LayerSet and DLabel objects!
2575 if (null != tmp) {
2576 if (tmp == active) top = true;
2577 if (top) al_top.add(tmp);
2578 else al_paint.add(tmp);
2580 while (ital.hasNext()) {
2581 final Displayable d = ital.next();
2582 if (d == active) top = true;
2583 if (top) al_top.add(d);
2584 else al_paint.add(d);
2587 return first_non_patch;
2590 @Deprecated
2591 public BufferedImage paintOffscreen(final Layer active_layer, final int g_width, final int g_height,
2592 final Rectangle srcRect, final double magnification, final Displayable active,
2593 final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
2594 final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
2595 final boolean prepaint, final ArrayList<Displayable> al_top) {
2596 return paintOffscreen(active_layer, active_layer.getParent().getColorCueLayerRange(active_layer), g_width, g_height, srcRect, magnification, active,
2597 c_alphas, clipRect, loader, hm, blending_list, mode, graphics_source,
2598 prepaint, al_top, false);
2601 /** This method uses data only from the arguments, and changes none.
2602 * Will fill @param al_top with proper Displayable objects, or none when none are selected. */
2603 public BufferedImage paintOffscreen(final Layer active_layer, final List<Layer> layers, final int g_width, final int g_height,
2604 final Rectangle srcRect, final double magnification, final Displayable active,
2605 final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
2606 final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
2607 final boolean prepaint, final ArrayList<Displayable> al_top, final boolean preload) {
2609 final ArrayList<Displayable> al_paint = new ArrayList<Displayable>();
2610 final int first_non_patch = gatherDisplayables(active_layer, layers, srcRect, active, al_paint, al_top, preload);
2612 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);
2615 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) {
2616 try {
2617 if (0 == g_width || 0 == g_height) return null;
2618 // ALMOST, but not always perfect //if (null != clipRect) g.setClip(clipRect);
2620 // prepare the canvas for the srcRect and magnification
2621 final AffineTransform atc = new AffineTransform();
2622 atc.scale(magnification, magnification);
2623 atc.translate(-srcRect.x, -srcRect.y);
2625 // the non-srcRect areas, in offscreen coords
2626 final Rectangle r1 = new Rectangle(srcRect.x + srcRect.width, srcRect.y, (int)(g_width / magnification) - srcRect.width, (int)(g_height / magnification));
2627 final Rectangle r2 = new Rectangle(srcRect.x, srcRect.y + srcRect.height, srcRect.width, (int)(g_height / magnification) - srcRect.height);
2629 // create new graphics
2630 try {
2631 display.getProject().getLoader().releaseToFit(g_width * g_height * 10);
2632 } catch (final Exception e) {} // when closing, asynch state may throw for a null loader.
2634 final BufferedImage target = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT); // creates a BufferedImage.TYPE_INT_ARGB image in my T60p ATI FireGL laptop
2635 //Utils.log2("offscreen acceleration priority: " + target.getAccelerationPriority());
2636 final Graphics2D g = target.createGraphics();
2638 g.setTransform(atc); //at_original);
2640 //setRenderingHints(g);
2641 // always a stroke of 1.0, regardless of magnification; the stroke below corrects for that
2642 g.setStroke(stroke);
2646 // Testing: removed Area.subtract, now need to fill in background
2647 g.setColor(Color.black);
2648 g.fillRect(0, 0, g_width - r1.x, g_height - r2.y);
2651 // paint:
2652 // 1 - background
2653 // 2 - images and anything else not on al_top
2654 // 3 - non-srcRect areas
2656 //Utils.log2("offscreen painting: " + al_paint.size());
2658 // filter paintables
2659 final Collection<? extends Paintable> paintables = graphics_source.asPaintable(al_paint);
2661 // adjust:
2662 first_non_patch = paintables.size() - (al_paint.size() - first_non_patch);
2664 // Determine painting mode
2665 if (Display.REPAINT_SINGLE_LAYER == mode) {
2666 if (display.isLiveFilteringEnabled()) {
2667 paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, true);
2668 } else {
2669 // Direct painting mode, with prePaint abilities
2670 int i = 0;
2671 for (final Paintable d : paintables) {
2672 if (i == first_non_patch) {
2673 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2674 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2675 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2676 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2677 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2678 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2680 if (prepaint) d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2681 else d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2682 i++;
2685 } else if (Display.REPAINT_MULTI_LAYER == mode) {
2686 // paint first the current layer Patches only (to set the background)
2687 // With prePaint capabilities:
2688 if (display.isLiveFilteringEnabled()) {
2689 paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, false);
2690 } else {
2691 int i = 0;
2692 if (prepaint) {
2693 for (final Paintable d : paintables) {
2694 if (first_non_patch == i) break;
2695 d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2696 i++;
2698 } else {
2699 for (final Paintable d : paintables) {
2700 if (first_non_patch == i) break;
2701 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2702 i++;
2707 // then blend on top the ImageData of the others, in reverse Z order and using the alpha of the LayerPanel
2708 final Composite original = g.getComposite();
2709 // reset
2710 g.setTransform(new AffineTransform());
2711 // Paint what:
2712 final Set<Class<?>> included = display.classes_to_multipaint;
2713 for (final ListIterator<LayerPanel> it = blending_list.listIterator(blending_list.size()); it.hasPrevious(); ) {
2714 final LayerPanel lp = it.previous();
2715 if (lp.layer == active_layer) continue;
2716 active_layer.getProject().getLoader().releaseToFit(g_width * g_height * 4 + 1024);
2717 final BufferedImage bi = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT);
2718 final Graphics2D gb = bi.createGraphics();
2719 gb.setTransform(atc);
2720 for (final Displayable d : lp.layer.find(srcRect, true)) {
2721 if (included.contains(d.getClass()))
2722 d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
2724 // Repeating loop ... the human compiler at work, just because one cannot lazily concatenate both sequences:
2725 for (final Displayable d : lp.layer.getParent().roughlyFindZDisplayables(lp.layer, srcRect, true)) {
2726 if (included.contains(d.getClass()))
2727 d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
2729 try {
2730 g.setComposite(Displayable.getComposite(display.getLayerCompositeMode(lp.layer), lp.getAlpha()));
2731 g.drawImage(display.applyFilters(bi), 0, 0, null);
2732 } catch (final Throwable t) {
2733 Utils.log("Could not use composite mode for layer overlays! Your graphics card may not support it.");
2734 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, lp.getAlpha()));
2735 g.drawImage(bi, 0, 0, null);
2736 IJError.print(t);
2738 bi.flush();
2740 // restore
2741 g.setComposite(original);
2742 g.setTransform(atc);
2744 // then paint the non-Patch objects of the current layer
2746 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2747 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2748 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2749 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2750 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2751 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2753 // TODO this loop should be reading from the paintable_patches and paintables, since their length/order *could* have changed
2754 // For the current layer:
2755 for (int i = first_non_patch; i < al_paint.size(); i++) {
2756 final Displayable d = al_paint.get(i);
2757 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2759 } else if(Display.REPAINT_RGB_LAYER == mode) {
2760 // TODO rewrite to avoid calling the list twice
2761 final Collection<? extends Paintable> paintable_patches = graphics_source.asPaintable(al_paint);
2763 final HashMap<Color,byte[]> channels = new HashMap<Color,byte[]>();
2764 hm.put(Color.green, active_layer);
2765 for (final Map.Entry<Color,Layer> e : hm.entrySet()) {
2766 final BufferedImage bi = new BufferedImage(g_width, g_height, BufferedImage.TYPE_BYTE_GRAY); //INDEXED, Loader.GRAY_LUT);
2767 final Graphics2D gb = bi.createGraphics();
2768 gb.setTransform(atc);
2769 final Layer la = e.getValue();
2770 final ArrayList<Paintable> list = new ArrayList<Paintable>();
2771 if (la == active_layer) {
2772 if (Color.green != e.getKey()) continue; // don't paint current layer in two channels
2773 list.addAll(paintable_patches);
2774 } else {
2775 list.addAll(la.find(Patch.class, srcRect, true));
2777 list.addAll(la.getParent().getZDisplayables(ImageData.class, true)); // Stack.class and perhaps others
2778 for (final Paintable d : list) {
2779 d.paint(gb, srcRect, magnification, false, c_alphas, la, layers);
2781 channels.put(e.getKey(), (byte[])new ByteProcessor(bi).getPixels());
2783 final byte[] red, green, blue;
2784 green = channels.get(Color.green);
2785 if (null == channels.get(Color.red)) red = new byte[green.length];
2786 else red = channels.get(Color.red);
2787 if (null == channels.get(Color.blue)) blue = new byte[green.length];
2788 else blue = channels.get(Color.blue);
2789 final int[] pix = new int[green.length];
2790 for (int i=0; i<green.length; i++) {
2791 pix[i] = ((red[i] & 0xff) << 16) + ((green[i] & 0xff) << 8) + (blue[i] & 0xff);
2793 // undo transform, is intended for Displayable objects
2794 g.setTransform(new AffineTransform());
2795 final ColorProcessor cp = new ColorProcessor(g_width, g_height, pix);
2796 if (display.invert_colors) cp.invert();
2797 display.applyFilters(cp);
2798 final Image img = cp.createImage();
2799 g.drawImage(img, 0, 0, null);
2800 img.flush();
2801 // reset
2802 g.setTransform(atc);
2804 // then paint the non-Image objects of the current layer
2805 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2806 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2807 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2808 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2809 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2810 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2812 for (final Displayable d : al_paint) {
2813 if (ImageData.class.isInstance(d)) continue;
2814 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2816 // TODO having each object type in a key/list<type> table would be so much easier and likely performant.
2819 // finally, paint non-srcRect areas
2820 if (r1.width > 0 || r1.height > 0 || r2.width > 0 || r2.height > 0) {
2821 g.setColor(Color.gray);
2822 g.setClip(r1);
2823 g.fill(r1);
2824 g.setClip(r2);
2825 g.fill(r2);
2828 return target;
2829 } catch (final OutOfMemoryError oome) {
2830 // so OutOfMemoryError won't generate locks
2831 IJError.print(oome);
2832 } catch (final Exception e) {
2833 IJError.print(e);
2835 return null;
2838 private final void paintWithFiltering(final Graphics2D g, final ArrayList<Displayable> al_paint,
2839 final Collection<? extends Paintable> paintables,
2840 final int first_non_patch,
2841 final int g_width, final int g_height,
2842 final Displayable active, final int c_alphas,
2843 final Layer layer, final List<Layer> layers, final boolean paint_non_images) {
2844 // Determine the type of the image: if any Patch is of type COLOR_RGB or COLOR_256, use RGB
2845 int type = BufferedImage.TYPE_BYTE_GRAY;
2846 search: for (final Displayable d : al_paint) {
2847 if (d.getClass() == Patch.class) {
2848 switch (((Patch)d).getType()) {
2849 case ImagePlus.COLOR_256:
2850 case ImagePlus.COLOR_RGB:
2851 type = BufferedImage.TYPE_INT_ARGB;
2852 break search;
2857 // Paint all patches to an image
2858 final BufferedImage bi = new BufferedImage(g_width, g_height, type);
2859 final Graphics2D gpre = bi.createGraphics();
2860 gpre.setTransform(atc);
2861 int i = 0;
2862 for (final Paintable p : paintables) {
2863 if (i == first_non_patch) break;
2864 p.paint(gpre, srcRect, magnification, p == active, c_alphas, layer, layers);
2865 i++;
2867 gpre.dispose();
2868 final ImagePlus imp = new ImagePlus("filtered", type == BufferedImage.TYPE_BYTE_GRAY ? new ByteProcessor(bi) : new ColorProcessor(bi));
2869 bi.flush();
2871 display.applyFilters(imp);
2873 // Paint the filtered image
2874 final AffineTransform aff = g.getTransform();
2875 g.setTransform(new AffineTransform()); // reset
2876 g.drawImage(imp.getProcessor().createImage(), 0, 0, null);
2877 // Paint the remaining elements if any
2878 if (paint_non_images && first_non_patch != paintables.size()) {
2879 g.setTransform(aff); // restore srcRect and magnification
2880 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2881 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2882 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2883 i = 0;
2884 for (final Paintable p : paintables) {
2885 if (i < first_non_patch) {
2886 i++;
2887 continue;
2889 p.paint(g, srcRect, magnification, p == active, c_alphas, layer, layers);
2890 i++;
2896 // added here to prevent flickering, but doesn't help. All it does is avoid a call to imp.redraw()
2897 @Override
2898 protected void scroll(final int sx, final int sy) {
2899 final int ox = xSrcStart + (int)(sx/magnification); //convert to offscreen coordinates
2900 final int oy = ySrcStart + (int)(sy/magnification);
2901 int newx = xSrcStart + (xMouseStart-ox);
2902 int newy = ySrcStart + (yMouseStart-oy);
2903 if (newx<0) newx = 0;
2904 if (newy<0) newy = 0;
2905 if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width;
2906 if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height;
2907 srcRect.x = newx;
2908 srcRect.y = newy;
2909 display.getMode().srcRectUpdated(srcRect, magnification);
2912 private void handleHide(final KeyEvent ke) {
2913 if (ke.isAltDown() && !ke.isShiftDown()) {
2914 // show hidden
2915 Display.updateCheckboxes(display.getLayer().getParent().setAllVisible(false), DisplayablePanel.VISIBILITY_STATE);
2916 //Display.repaint(display.getLayer());
2917 Display.update(display.getLayer());
2918 ke.consume();
2919 return;
2921 if (ke.isShiftDown()) {
2922 // hide deselected
2923 display.hideDeselected(ke.isAltDown());
2924 ke.consume();
2925 return;
2927 // else, hide selected
2928 display.getSelection().setVisible(false);
2929 Display.update(display.getLayer());
2930 ke.consume();
2933 DisplayCanvas.Screenshot createScreenshot(final Layer layer) {
2934 return new Screenshot(layer);
2937 protected class ScreenshotProperties {
2938 final Layer layer;
2939 final Rectangle srcRect;
2940 final double magnification;
2941 final int g_width, g_height, c_alphas;
2942 final GraphicsSource graphics_source;
2943 final ArrayList<LayerPanel> blending_list;
2944 final HashMap<Color,Layer> hm;
2945 final int mode;
2946 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) {
2947 this.srcRect = new Rectangle(srcRect);
2948 this.magnification = magnification;
2949 this.layer = layer;
2950 this.blending_list = new ArrayList<LayerPanel>();
2951 this.hm = new HashMap<Color,Layer>();
2952 this.mode = display.getPaintMode(hm, blending_list);
2953 this.g_width = g_width;
2954 this.g_height = g_height;
2955 this.graphics_source = graphics_source;
2956 this.c_alphas = c_alphas;
2957 final Layer current_layer = display.getLayer();
2958 if (Display.REPAINT_RGB_LAYER == mode) {
2959 final Layer red = hm.get(Color.red);
2960 final Layer blue = hm.get(Color.blue);
2961 if (null != red || null != blue) {
2962 final LayerSet ls = layer.getParent();
2963 final int i_layer = ls.indexOf(layer);
2964 final int i_current = ls.indexOf(current_layer);
2965 if (null != red) {
2966 final int i_red = ls.indexOf(red);
2967 final Layer l = red.getParent().getLayer(i_red + i_current - i_layer);
2968 if (null != l) {
2969 hm.put(Color.red, l);
2970 } else {
2971 hm.remove(Color.red);
2974 if (null != blue) {
2975 final int i_blue = ls.indexOf(blue);
2976 final Layer l = blue.getParent().getLayer(i_blue + i_current - i_layer);
2977 if (null != l) {
2978 hm.put(Color.blue, l);
2979 } else {
2980 hm.remove(Color.blue);
2986 @Override
2987 public final boolean equals(final Object o) {
2988 final ScreenshotProperties s = (ScreenshotProperties)o;
2989 return s.layer == this.layer
2990 && s.magnification == this.magnification
2991 && s.srcRect.x == this.srcRect.x && s.srcRect.y == this.srcRect.y
2992 && s.srcRect.width == this.srcRect.width && s.srcRect.height == this.srcRect.height
2993 && s.mode == this.mode
2994 && s.c_alphas == this.c_alphas
2995 && Utils.equalContent(s.blending_list, this.blending_list)
2996 && Utils.equalContent(s.hm, this.hm);
2998 @Override
2999 public int hashCode() { return 0; } //$%^&$#@!
3002 public class Screenshot {
3003 final Layer layer;
3004 long sid = Long.MIN_VALUE;
3005 long born = 0;
3006 final ArrayList<Displayable> al_top = new ArrayList<Displayable>();
3007 final ScreenshotProperties props;
3009 Screenshot(final Layer layer) {
3010 this(layer, DisplayCanvas.this.srcRect, DisplayCanvas.this.magnification, DisplayCanvas.this.getWidth(), DisplayCanvas.this.getHeight(), DisplayCanvas.this.display.getDisplayChannelAlphas(), DisplayCanvas.this.display.getMode().getGraphicsSource());
3013 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) {
3014 this.layer = layer;
3015 this.props = new ScreenshotProperties(layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source);
3018 public long init() {
3019 this.born = System.currentTimeMillis();
3020 this.sid = layer.getProject().getLoader().getNextTempId();
3021 return this.sid;
3023 /** Associate @param img to this, with a new sid. */
3024 public long assoc(final BufferedImage img) {
3025 init();
3026 if (null != img) layer.getProject().getLoader().cacheAWT(this.sid, img);
3027 return this.sid;
3029 public void createImage() {
3030 final BufferedImage img = paintOffscreen(layer, layer.getParent().getColorCueLayerRange(layer), props.g_width, props.g_height, props.srcRect, props.magnification,
3031 display.getActive(), props.c_alphas, null, layer.getProject().getLoader(),
3032 props.hm, props.blending_list, props.mode, props.graphics_source, false, al_top, false);
3033 layer.getProject().getLoader().cacheAWT(sid, img);
3035 public void flush() {
3036 layer.getProject().getLoader().decacheAWT(sid);
3040 private boolean browseToNodeLayer(final boolean is_shift_down) {
3041 // find visible instances of Tree that are currently painting in the canvas
3042 try {
3043 final Layer active_layer = display.getLayer();
3044 final Point po = getCursorLoc(); // in offscreen coords
3045 for (final ZDisplayable zd : display.getLayerSet().getDisplayableList()) {
3046 if (!zd.isVisible()) continue;
3047 if (!(zd instanceof Tree<?>)) continue;
3048 final Tree<?> t = (Tree<?>)zd;
3049 final Layer la = t.toClosestPaintedNode(active_layer, po.x, po.y, magnification);
3050 if (null == la) continue;
3051 // Else:
3052 display.toLayer(la);
3053 if (!is_shift_down) display.getSelection().clear();
3054 display.getSelection().add(t);
3055 switch (ProjectToolbar.getToolId()) {
3056 case ProjectToolbar.PEN:
3057 case ProjectToolbar.BRUSH:
3058 break;
3059 default:
3060 ProjectToolbar.setTool(ProjectToolbar.PEN);
3061 break;
3063 return true;
3065 } catch (final Exception e) {
3066 Utils.log2("Oops: " + e);
3068 return false;
3071 /** Smoothly move the canvas from x0,y0,layer0 to x1,y1,layer1 */
3072 protected void animateBrowsing(final int dx, final int dy) {
3073 // check preconditions
3074 final float mag = (float)this.magnification;
3075 final Rectangle startSrcRect = (Rectangle)this.srcRect.clone();
3076 // The motion will be displaced by some screen pixels at every time step.
3077 final Vector2f v = new Vector2f(dx, dy);
3078 final float sqdist_to_travel = v.lengthSquared();
3079 v.normalize();
3080 v.scale(20/mag);
3081 final Point2f cp = new Point2f(0, 0); // the current deltas
3083 final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
3084 sf[0] = animate(new Runnable() {
3085 @Override
3086 public void run() {
3087 cp.add(v);
3088 //Utils.log2("advanced by x,y = " + cp.x + ", " + cp.y);
3089 int x, y;
3090 if (v.lengthSquared() >= sqdist_to_travel) {
3091 // set target position
3092 x = startSrcRect.x + dx;
3093 y = startSrcRect.y + dy;
3094 // quit animation
3095 cancelAnimation(sf[0]);
3096 } else {
3097 // set position
3098 x = startSrcRect.x + (int)(cp.x);
3099 y = startSrcRect.y + (int)(cp.y);
3101 setSrcRect(x, y, startSrcRect.width, startSrcRect.height);
3102 display.repaintAll2();
3104 }, 0, 50, TimeUnit.MILLISECONDS);
3107 /** Smoothly move the canvas from its current position until the given rectangle is included within the srcRect.
3108 * If the given rectangle is larger than the srcRect, it will refuse to work (for now). */
3109 public boolean animateBrowsing(final Rectangle target_, final Layer target_layer) {
3110 // Crop target to world's 2D dimensions
3111 final Area a = new Area(target_);
3112 a.intersect(new Area(display.getLayerSet().get2DBounds()));
3113 final Rectangle target = a.getBounds();
3114 if (0 == target.width || 0 == target.height) {
3115 return false;
3117 // animate at all?
3118 if (this.srcRect.contains(target) && target_layer == display.getLayer()) {
3119 // So: don't animate, but at least highlight the target
3120 playHighlight(target);
3121 return false;
3124 // The motion will be displaced by some screen pixels at every time step.
3125 final int ox = srcRect.x + srcRect.width/2;
3126 final int oy = srcRect.y + srcRect.height/2;
3127 final int tx = target.x + target.width/2;
3128 final int ty = target.y + target.height/2;
3129 final Vector2f v = new Vector2f(tx - ox, ty - oy);
3130 v.normalize();
3131 v.scale(20/(float)magnification);
3134 // The layer range
3135 final Layer start_layer = display.getLayer();
3137 int ithis = display.getLayerSet().indexOf(start_layer);
3138 int itarget = display.getLayerSet().indexOf(target_layer);
3139 final java.util.List<Layer> layers = display.getLayerSet().getLayers(ithis, itarget);
3141 final Calibration cal = display.getLayerSet().getCalibrationCopy();
3142 final double pixelWidth = cal.pixelWidth;
3143 final double pixelHeight = cal.pixelHeight;
3145 //final double dist_to_travel = Math.sqrt(Math.pow((tx - ox)*pixelWidth, 2) + Math.pow((ty - oy)*pixelHeight, 2)
3146 // + Math.pow((start_layer.getZ() - target_layer.getZ()) * pixelWidth, 2));
3148 // vector in calibrated coords between origin and target
3149 final Vector3d g = new Vector3d((tx - ox)*pixelWidth, (ty - oy)*pixelHeight, (target_layer.getZ() - start_layer.getZ())*pixelWidth);
3151 final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
3152 sf[0] = animate(new Runnable() {
3153 @Override
3154 public void run() {
3155 if (DisplayCanvas.this.srcRect.contains(target)) {
3156 // reached destination
3157 if (display.getLayer() != target_layer) display.toLayer(target_layer);
3158 playHighlight(target);
3159 cancelAnimation(sf[0]);
3160 } else {
3161 setSrcRect(srcRect.x + (int)v.x, srcRect.y + (int)v.y, srcRect.width, srcRect.height);
3162 // which layer?
3163 if (start_layer != target_layer) {
3164 final int cx = srcRect.x + srcRect.width/2;
3165 final int cy = srcRect.y + srcRect.height/2;
3166 final double dist = Math.sqrt(Math.pow((cx - ox)*pixelWidth, 2) + Math.pow((cy - oy)*pixelHeight, 2)
3167 + Math.pow((display.getLayer().getZ() - start_layer.getZ()) * pixelWidth, 2));
3169 final Vector3d gg = new Vector3d(g);
3170 gg.normalize();
3171 gg.scale((float)dist);
3172 final Layer la = display.getLayerSet().getNearestLayer(start_layer.getZ() + gg.z/pixelWidth);
3173 if (la != display.getLayer()) {
3174 display.toLayer(la);
3177 display.repaintAll2();
3180 }, 0, 50, TimeUnit.MILLISECONDS);
3181 return true;
3184 private ScheduledExecutorService animator = null;
3185 private boolean zoom_and_pan = true;
3186 private final Vector<ScheduledFuture<?>> sfs = new Vector<ScheduledFuture<?>>();
3188 private void cancelAnimations() {
3189 if (sfs.isEmpty()) return;
3190 Vector<ScheduledFuture<?>> sfs;
3191 synchronized (this.sfs) { sfs = new Vector<ScheduledFuture<?>>(this.sfs); }
3192 for (final ScheduledFuture<?> sf : sfs) {
3193 sf.cancel(true);
3195 this.sfs.clear();
3196 try {
3197 // wait
3198 Thread.sleep(150);
3199 } catch (final InterruptedException ie) {}
3200 // Re-enable input, in case the watcher task is canceled as well:
3201 // (It's necessary since there isn't any easy way to tell the scheduler to execute a code block when it cancels its tasks).
3202 restoreUserInput();
3204 private void cancelAnimation(final ScheduledFuture<?> sf) {
3205 sfs.remove(sf);
3206 sf.cancel(true);
3207 restoreUserInput();
3210 private void restoreUserInput() {
3211 zoom_and_pan = true;
3212 display.getProject().setReceivesInput(true);
3215 private ScheduledFuture<?> animate(final Runnable run, final long initialDelay, final long delay, final TimeUnit units) {
3216 initAnimator();
3217 // Cancel any animations currently running
3218 cancelAnimations();
3219 // Disable user input
3220 display.getProject().setReceivesInput(false);
3221 zoom_and_pan = false;
3222 // Create tasks to run periodically: a task and a watcher task
3223 final ScheduledFuture<?>[] sf = new ScheduledFuture[2];
3224 sf[0] = animator.scheduleWithFixedDelay(run, initialDelay, delay, units);
3225 sf[1] = animator.scheduleWithFixedDelay(new Runnable() {
3226 @Override
3227 public void run() {
3228 if (sf[0].isCancelled()) {
3229 // Enable user input
3230 zoom_and_pan = true;
3231 display.getProject().setReceivesInput(true);
3232 // cancel yourself
3233 sf[1].cancel(true);
3236 }, 100, 700, TimeUnit.MILLISECONDS);
3237 // Store task for future cancelation
3238 sfs.add(sf[0]);
3239 // but not the watcher task, which must finish on its own after the main task finishes.
3240 return sf[0];
3243 /** Draw a dotted circle centered on the given Rectangle. */
3244 private final class Highlighter {
3245 Ellipse2D.Float elf;
3246 final Stroke stroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3, new float[]{4,4,4,4}, 0);
3247 final float dec;
3248 final Rectangle target;
3249 Highlighter(final Rectangle target) {
3250 this.target = target;
3251 elf = new Ellipse2D.Float(target.x, target.y, target.width, target.height);
3252 display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
3253 dec = (float)((Math.max(target.width, target.height)*magnification / 10)/magnification);
3255 boolean next() {
3256 invalidateVolatile();
3257 repaint(target, 5, false);
3258 // setup next iteration
3259 display.getLayerSet().getOverlay().remove(elf);
3260 final Ellipse2D.Float elf2 = (Ellipse2D.Float) elf.clone();
3261 elf2.x += dec;
3262 elf2.y += dec;
3263 elf2.width -= (dec+dec);
3264 elf2.height -= (dec+dec);
3265 if (elf2.width > 1 || elf2.height > 1) {
3266 elf = elf2;
3267 display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
3268 return true;
3269 } else {
3270 display.getLayerSet().getOverlay().remove(elf);
3271 return false;
3274 void cleanup() {
3275 display.getLayerSet().getOverlay().remove(elf);
3279 private interface Animation extends Runnable {}
3281 private ScheduledFuture<?> playHighlight(final Rectangle target) {
3282 initAnimator();
3283 final Highlighter highlight = new Highlighter(target);
3284 final ScheduledFuture<?>[] sf = (ScheduledFuture<?>[])new ScheduledFuture[2];
3285 sf[0] = animator.scheduleWithFixedDelay(new Animation() {
3286 @Override
3287 public void run() {
3288 if (!highlight.next()) {
3289 cancelAnimation(sf[0]);
3290 highlight.cleanup();
3293 }, 10, 100, TimeUnit.MILLISECONDS);
3294 sf[1] = animator.scheduleWithFixedDelay(new Animation() {
3295 @Override
3296 public void run() {
3297 if (sf[0].isCancelled()) {
3298 highlight.cleanup();
3299 sf[1].cancel(true); // itself
3302 }, 50, 100, TimeUnit.MILLISECONDS);
3303 sfs.add(sf[0]);
3304 return sf[0];
3307 synchronized private void initAnimator() {
3308 if (null == animator) animator = Executors.newScheduledThreadPool(2);