Removed DisplayCanvas.setDrawingColor(int,int,boolean) since it exclusively
[trakem2.git] / ini / trakem2 / display / DisplayCanvas.java
blobd6f0455d3c2bd332a076af2f5594f6f741a54903
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 ij.IJ;
26 import ij.ImagePlus;
27 import ij.Prefs;
28 import ij.WindowManager;
29 import ij.gui.ImageCanvas;
30 import ij.gui.Roi;
31 import ij.gui.Toolbar;
32 import ij.measure.Calibration;
33 import ij.process.ByteProcessor;
34 import ij.process.ColorProcessor;
35 import ini.trakem2.Project;
36 import ini.trakem2.display.graphics.GraphicsSource;
37 import ini.trakem2.display.inspect.InspectPatchTrianglesMode;
38 import ini.trakem2.imaging.Segmentation;
39 import ini.trakem2.persistence.Loader;
40 import ini.trakem2.utils.Bureaucrat;
41 import ini.trakem2.utils.IJError;
42 import ini.trakem2.utils.Lock;
43 import ini.trakem2.utils.ProjectToolbar;
44 import ini.trakem2.utils.Search;
45 import ini.trakem2.utils.Utils;
46 import ini.trakem2.utils.Worker;
48 import java.awt.AWTException;
49 import java.awt.AlphaComposite;
50 import java.awt.BasicStroke;
51 import java.awt.Color;
52 import java.awt.Component;
53 import java.awt.Composite;
54 import java.awt.Cursor;
55 import java.awt.Event;
56 import java.awt.Graphics;
57 import java.awt.Graphics2D;
58 import java.awt.GraphicsConfiguration;
59 import java.awt.Image;
60 import java.awt.Point;
61 import java.awt.Rectangle;
62 import java.awt.RenderingHints;
63 import java.awt.Robot;
64 import java.awt.Stroke;
65 import java.awt.Toolkit;
66 import java.awt.Transparency;
67 import java.awt.event.InputEvent;
68 import java.awt.event.KeyEvent;
69 import java.awt.event.KeyListener;
70 import java.awt.event.MouseEvent;
71 import java.awt.event.MouseWheelEvent;
72 import java.awt.event.MouseWheelListener;
73 import java.awt.geom.AffineTransform;
74 import java.awt.geom.Area;
75 import java.awt.geom.Ellipse2D;
76 import java.awt.image.BufferedImage;
77 import java.awt.image.PixelGrabber;
78 import java.awt.image.VolatileImage;
79 import java.util.ArrayList;
80 import java.util.Collection;
81 import java.util.Collections;
82 import java.util.HashMap;
83 import java.util.HashSet;
84 import java.util.Hashtable;
85 import java.util.Iterator;
86 import java.util.List;
87 import java.util.ListIterator;
88 import java.util.Map;
89 import java.util.Set;
90 import java.util.Vector;
91 import java.util.concurrent.Executors;
92 import java.util.concurrent.ScheduledExecutorService;
93 import java.util.concurrent.ScheduledFuture;
94 import java.util.concurrent.TimeUnit;
96 import javax.vecmath.Point2f;
97 import javax.vecmath.Vector2f;
98 import javax.vecmath.Vector3d;
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 String openglProperty = System.getProperty("sun.java2d.opengl");
148 openglEnabled = openglProperty != null && Boolean.parseBoolean(openglProperty);
149 } catch (Exception ex) { }
150 try {
151 String quartzProperty = System.getProperty("apple.awt.graphics.UseQuartz");
152 quartzEnabled = Boolean.parseBoolean(quartzProperty);
153 } catch (Exception ex) { }
154 try {
155 String ddscaleProperty = System.getProperty("sun.java2d.ddscale");
156 String d3dProperty = System.getProperty("sun.java2d.d3d");
157 ddscaleEnabled = Boolean.parseBoolean(ddscaleProperty) && Boolean.parseBoolean(d3dProperty);
158 } catch (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(Display display, int width, 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(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(double mag, 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(double width, 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 public final void update(final Graphics g) {
364 // 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.
365 this.paint(g);
368 /** Handles repaint event requests and the generation of offscreen threads. */
369 private final AbstractRepaintThread RT = new AbstractRepaintThread(this, "T2-Canvas-Repainter", new OffscreenThread()) {
370 protected void handleUpdateGraphics(final Component target, final Rectangle clipRect) {
371 final Layer active_layer = display.getLayer();
372 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()));
377 private final void setRenderingHints(final Graphics2D g) {
378 // so slow!! Particularly the first one.
379 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
380 //g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
381 //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
382 //g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
383 //g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
387 public void setMagnification(double mag) {
388 if (mag < 0.00000001) mag = 0.00000001;
389 // ensure a stroke of thickness 1.0 regardless of magnification
390 this.stroke = new BasicStroke((float)(1.0/mag), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
391 // FIXES MAG TO ImageCanvas.zoomLevel LIMITS!!
392 //super.setMagnification(mag);
393 // So, manually:
394 this.magnification = mag;
395 imp.setTitle(imp.getTitle());
396 display.getMode().magnificationUpdated(srcRect, mag);
399 /** Paint lines always with a thickness of 1 pixel. This stroke is modified when the magnification is changed, to compensate. */
400 private BasicStroke stroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
402 /** The affine transform representing the srcRect displacement and the magnification. */
403 private final AffineTransform atc = new AffineTransform();
405 public void paint(final Graphics g) {
406 if (null == g) return;
407 try {
408 synchronized (lock_paint) {
409 lock_paint.lock();
412 // ensure proper positioning
413 g.translate(0, 0); // ints!
415 final Rectangle clipRect = g.getClipBounds();
417 final Displayable active = display.getActive();
418 final int c_alphas = display.getDisplayChannelAlphas();
420 final Layer active_layer = display.getLayer();
421 final List<Layer> layers = active_layer.getParent().getColorCueLayerRange(active_layer);
423 final Graphics2D g2d = (Graphics2D)g;
425 // prepare the canvas for the srcRect and magnification
426 final AffineTransform at_original = g2d.getTransform();
427 atc.setToIdentity();
428 atc.scale(magnification, magnification);
429 atc.translate(-srcRect.x, -srcRect.y);
430 at_original.preConcatenate(atc);
432 if (null != offscreen && dragging) invalidateVolatile(); // to update the active at least
433 render(g, active, active_layer, layers, c_alphas, at_original, clipRect);
435 g2d.setTransform(at_original);
437 g2d.setStroke(this.stroke);
439 // debug buckets
440 //if (null != display.getLayer().root) display.getLayer().root.paint(g2d, srcRect, magnification, Color.red);
441 //if (null != display.getLayer().getParent().lbucks.get(display.getLayer()).root) display.getLayer().getParent().lbucks.get(display.getLayer()).root.paint(g2d, srcRect, magnification, Color.blue);
444 // reset to identity
445 g2d.setTransform(new AffineTransform());
446 // reset to 1.0 thickness
447 g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
449 // paint brush outline for AreaList, or fast-marching area
450 if (mouse_in && null != active && AreaContainer.class.isInstance(active)) {
451 switch (ProjectToolbar.getToolId()) {
452 case ProjectToolbar.BRUSH:
453 int brushSize = ProjectToolbar.getBrushSize();
454 g.setColor(active.getColor());
455 g.drawOval((int)((xMouse -srcRect.x -brushSize/2)*magnification), (int)((yMouse - srcRect.y -brushSize/2)*magnification), (int)(brushSize * magnification), (int)(brushSize * magnification));
456 break;
457 case ProjectToolbar.PENCIL:
458 case ProjectToolbar.WAND:
459 Composite co = g2d.getComposite();
460 if (IJ.isWindows()) g2d.setColor(Color.yellow);
461 else g2d.setXORMode(Color.yellow); // XOR on yellow for best contrast
462 g2d.drawRect((int)((xMouse -srcRect.x -Segmentation.fmp.width/2) * magnification),
463 (int)((yMouse -srcRect.y -Segmentation.fmp.height/2) * magnification),
464 (int)(Segmentation.fmp.width * magnification),
465 (int)(Segmentation.fmp.height * magnification));
466 g2d.setComposite(co); // undo XOR mode
467 break;
472 final Roi roi = imp.getRoi();
473 if (null != roi) {
474 roi.draw(g);
477 // Mathias code:
478 if (null != freehandProfile) {
479 freehandProfile.paint(g, magnification, srcRect, true);
480 if(noCursor == null)
481 noCursor = Toolkit.getDefaultToolkit().createCustomCursor(new BufferedImage(1,1,BufferedImage.TYPE_BYTE_BINARY), new Point(0,0), "noCursor");
484 } catch (Exception e) {
485 Utils.log2("DisplayCanvas.paint(Graphics) Error: " + e);
486 IJError.print(e);
487 } finally {
488 synchronized (lock_paint) {
489 lock_paint.unlock();
494 public void waitForRepaint() {
495 // wait for all offscreen methods to finish painting
496 RT.waitForOffs();
497 // wait for the paint method to finish painting
498 synchronized (lock_paint) {
499 lock_paint.lock();
500 lock_paint.unlock();
504 /** Paints a handle on the screen coords. Adapted from ij.gui.Roi class. */
505 static public void drawHandle(final Graphics g, final int x, final int y, final double magnification) {
506 final int width5 = (int)(5 / magnification + 0.5);
507 final int width3 = (int)(3 / magnification + 0.5);
508 final int corr2 = (int)(2 / magnification + 0.5);
509 final int corr1 = (int)(1 / magnification + 0.5);
510 g.setColor(Color.white);
511 g.drawRect(x - corr2, y - corr2, width5, width5);
512 g.setColor(Color.black);
513 g.drawRect(x - corr1, y - corr1, width3, width3);
514 g.setColor(Color.white);
515 g.fillRect(x, y, corr1, corr1);
518 /** Paints a handle at x,y screen coords. */
519 static public void drawScreenHandle(final Graphics g, final int x, final int y) {
520 g.setColor(Color.orange);
521 g.drawRect(x - 2, y - 2, 5, 5);
522 g.setColor(Color.black);
523 g.drawRect(x - 1, y - 1, 3, 3);
524 g.setColor(Color.orange);
525 g.fillRect(x, y, 1, 1);
528 /** Paints a handle on the offscreen x,y. Adapted from ij.gui.Roi class. */
530 private void drawHandle(Graphics g, double x, double y) {
531 g.setColor(Color.black);
532 g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
533 g.setColor(Color.white);
534 g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);
538 static protected BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
539 static protected AffineTransform DEFAULT_AFFINE = new AffineTransform();
541 static public void drawHandle(Graphics2D g, double x, double y, Rectangle srcRect, double magnification) {
542 AffineTransform original = g.getTransform();
543 g.setTransform(DEFAULT_AFFINE);
544 Stroke st = g.getStroke();
545 g.setStroke(DEFAULT_STROKE);
547 g.setColor(Color.black);
548 g.fillRect((int) ((x - srcRect.x) * magnification) - 1, (int) ((y - srcRect.y) * magnification) - 1, 3, 3);
549 g.setColor(Color.white);
550 g.drawRect((int) ((x - srcRect.x) * magnification) - 2, (int) ((y - srcRect.y) * magnification) - 2, 5, 5);
552 g.setStroke(st);
553 g.setTransform(original);
556 /** As offscreen. */
557 private int x_p, y_p, x_d, y_d, x_d_old, y_d_old;
559 private boolean popup = false;
561 private boolean locked = false;
563 private int tmp_tool = -1;
565 /** In world coordinates. */
566 protected Point last_popup = null;
568 protected Point consumeLastPopupPoint() {
569 Point p = last_popup;
570 last_popup = null;
571 return p;
574 public void mousePressed(MouseEvent me) {
576 super.flags = me.getModifiers();
578 x_p = x_d = srcRect.x + (int) (me.getX() / magnification); // offScreenX(me.getX());
579 y_p = y_d = srcRect.y + (int) (me.getY() / magnification); // offScreenY(me.getY());
581 this.xMouse = x_p;
582 this.yMouse = y_p;
584 // ban if beyond bounds:
585 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
586 return;
589 // Popup:
590 popup = false; // not reset properly in macosx
591 if (Utils.isPopupTrigger(me)) {
592 popup = true;
593 last_popup = new Point(x_p, y_p);
594 display.getPopupMenu().show(this, me.getX(), me.getY());
595 return;
598 // reset
599 snapping = false;
601 int tool = ProjectToolbar.getToolId();
603 // pan with middle mouse like in inkscape
604 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
605 if (0 != (flags & InputEvent.BUTTON2_MASK))
607 if (me.getButton() == MouseEvent.BUTTON2) {
608 tmp_tool = tool;
609 ProjectToolbar.setTool(Toolbar.HAND);
610 tool = Toolbar.HAND;
612 //Utils.log2("button: " + me.getButton() + " BUTTON2: " + MouseEvent.BUTTON2);
614 if (!zoom_and_pan) {
615 // stop animations when clicking (and on pushing ESC)
616 cancelAnimations();
619 switch (tool) {
620 case Toolbar.MAGNIFIER:
621 if (me.isAltDown()) zoomOut(me.getX(), me.getY());
622 else zoomIn(me.getX(), me.getY());
623 return;
624 case Toolbar.HAND:
625 super.setupScroll(x_p, y_p); // offscreen coords.
626 return;
629 if (input_disabled) {
630 input_disabled2 = true;
631 Utils.showMessage("Please wait while completing the task.\nOnly the glass and hand tool are enabled.");
632 return; // only zoom and pan are allowed
635 Displayable active = display.getActive();
637 if (isTransforming() && ProjectToolbar.SELECT != tool) {
638 Utils.logAll("Notice: the 'Select' tool is not active!\n Activate the 'Select' tool to operate transformation modes.");
641 switch (tool) {
642 case ProjectToolbar.PENCIL:
643 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
644 Profile prof = (Profile) active;
645 this.freehandProfile = new FreeHandProfile(prof);
646 freehandProfile.mousePressed(x_p, y_p);
647 return;
649 break;
650 case Toolbar.RECTANGLE:
651 case Toolbar.OVAL:
652 case Toolbar.POLYGON:
653 case Toolbar.FREEROI:
654 case Toolbar.LINE:
655 case Toolbar.POLYLINE:
656 case Toolbar.FREELINE:
657 case Toolbar.ANGLE:
658 case Toolbar.POINT:
659 // pass the mouse event to superclass ImageCanvas.
660 super.mousePressed(me);
661 repaint();
662 return;
663 case Toolbar.DROPPER:
664 // The color dropper
665 setDrawingColor(x_p, y_p, me.isAltDown());
666 return;
669 // check:
670 if (display.isReadOnly()) return;
672 switch (tool) {
673 case Toolbar.TEXT:
674 if (!isTransforming()) {
675 // edit a label, or add a new one
676 if (null == active || !active.contains(x_p, y_p)) {
677 // find a Displayable to activate, if any
678 display.choose(me.getX(), me.getY(), x_p, y_p, DLabel.class);
679 active = display.getActive();
681 if (null != active && active.isVisible() && active instanceof DLabel) {
682 // edit
683 ((DLabel) active).edit();
684 } else {
685 // new
686 DLabel label = new DLabel(display.getProject(), " ", x_p, y_p);
687 display.getLayer().add(label);
688 label.edit();
691 return;
694 // SPECIFIC for SELECT and above tools
696 // no ROIs allowed past this point
697 if (tool >= ProjectToolbar.SELECT) imp.killRoi();
698 else return;
700 Selection selection = display.getSelection();
701 if (isTransforming()) {
702 box = display.getMode().getRepaintBounds();
703 display.getMode().mousePressed(me, x_p, y_p, magnification);
704 return;
706 // select or deselect another active Displayable, or add it to the selection group:
707 if (ProjectToolbar.SELECT == tool) {
708 display.choose(me.getX(), me.getY(), x_p, y_p, me.isShiftDown(), null);
710 active = display.getActive();
711 selection = display.getSelection();
713 if (null == active || !active.isVisible()) return;
715 switch (tool) {
716 case ProjectToolbar.SELECT:
717 // gather initial box (for repainting purposes)
718 box = display.getMode().getRepaintBounds();
719 // check if the active is usable:
720 // check if the selection contains locked objects
721 if (selection.isLocked()) {
722 locked = true;
723 return;
725 display.getMode().mousePressed(me, x_p, y_p, magnification);
726 break;
727 default: // the PEN and PENCIL tools, and any other custom tool
728 display.getLayerSet().addPreDataEditStep(active);
729 box = active.getBoundingBox();
730 active.mousePressed(me, display.getLayer(), x_p, y_p, magnification);
731 invalidateVolatile();
732 break;
736 public void mouseDragged(MouseEvent me) {
738 super.flags = me.getModifiers();
740 if (popup) return;
742 // ban if beyond bounds:
743 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
744 return;
747 if (ProjectToolbar.SELECT == ProjectToolbar.getToolId() && locked) {
748 Utils.log2("Selection is locked.");
749 return;
752 dragging = true;
754 x_d_old = x_d;
755 y_d_old = y_d;
757 x_d = srcRect.x + (int) (me.getX() / magnification); // offscreen
758 y_d = srcRect.y + (int) (me.getY() / magnification);
760 this.xMouse = x_d;
761 this.yMouse = y_d;
763 // protection:
764 int me_x = me.getX();
765 int me_y = me.getY();
766 if (me_x < 0 || me_x > this.getWidth() || me_y < 0 || me_y > this.getHeight()) {
767 x_d = x_d_old;
768 y_d = y_d_old;
769 return;
772 int tool = ProjectToolbar.getToolId();
775 // pan with middle mouse like in inkscape
776 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
777 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
778 tool = Toolbar.HAND;
780 */ // so the above has been implemented as a temporary switch to the HAND tool at the mousePressed function.
782 switch (tool) {
783 case Toolbar.MAGNIFIER: // TODO : create a zooms-area tool
784 return;
785 case Toolbar.HAND:
786 int srx = srcRect.x,
787 sry = srcRect.y;
788 scroll(me.getX(), me.getY());
789 if (0 != srx - srcRect.x || 0 != sry - srcRect.y) {
790 update_graphics = true; // update the offscreen images.
791 display.getNavigator().repaint(false);
792 repaint(true);
794 return;
797 if (input_disabled2) return;
799 //debug:
800 //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));
802 // Code for Matthias' FreehandProfile (TODO this should be done on mousePressed, not on mouseDragged)
804 Displayable active = display.getActive();
805 if (null != active && active.getClass() == Profile.class) {
806 try {
807 if (r == null) {
808 r = new Robot(this.getGraphicsConfiguration().getDevice());
810 } catch (AWTException e) {
811 e.printStackTrace();
815 switch (tool) {
816 case ProjectToolbar.PENCIL:
817 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
818 if (freehandProfile == null)
819 return; // starting painting out of the DisplayCanvas border
820 double dx = x_d - x_d_old;
821 double dy = y_d - y_d_old;
822 freehandProfile.mouseDragged(me, x_d, y_d, dx, dy);
823 repaint();
824 // Point screenLocation = getLocationOnScreen();
825 // mousePos[0] += screenLocation.x;
826 // mousePos[1] += screenLocation.y;
827 // r.mouseMove( mousePos[0], mousePos[1]);
828 return;
830 break;
831 case Toolbar.RECTANGLE:
832 case Toolbar.OVAL:
833 case Toolbar.POLYGON:
834 case Toolbar.FREEROI:
835 case Toolbar.LINE:
836 case Toolbar.POLYLINE:
837 case Toolbar.FREELINE:
838 case Toolbar.ANGLE:
839 case Toolbar.POINT:
840 // pass the mouse event to superclass ImageCanvas.
841 super.mouseDragged(me);
842 repaint(false);
843 return;
845 // no ROIs beyond this point
846 if (tool >= ProjectToolbar.SELECT) imp.killRoi();
847 else return;
849 // check:
850 if (display.isReadOnly()) return;
852 if (null != active && active.isVisible()) {
853 // prevent dragging beyond the layer limits
854 if (display.getLayer().contains(x_d, y_d, 1)) {
855 Rectangle box2;
856 switch (tool) {
857 case ProjectToolbar.SELECT:
858 display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
859 box2 = display.getMode().getRepaintBounds();
860 box.add(box2);
861 // repaint all Displays (where it was and where it is now, hence the sum of both boxes):
862 Display.repaint(display.getLayer(), Selection.PADDING, box, false, active.isLinked() || active.getClass() == Patch.class);
863 // box for next mouse dragged iteration
864 box = box2;
865 break;
866 default:
867 active.mouseDragged(me, display.getLayer(), x_p, y_p, x_d, y_d, x_d_old, y_d_old);
868 // the line above must repaint on its own
869 break;
871 } else {
872 //beyond_srcRect = true;
873 Utils.log("DisplayCanvas.mouseDragged: preventing drag beyond layer limits.");
875 } else if (display.getMode() instanceof ManualAlignMode
876 || display.getMode() instanceof InspectPatchTrianglesMode) {
877 if (display.getLayer().contains(x_d, y_d, 1)) {
878 if (tool >= ProjectToolbar.SELECT) {
879 display.getMode().mouseDragged(me, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
885 public void mouseReleased(MouseEvent me) {
887 super.flags = me.getModifiers();
889 boolean dragging2 = dragging;
890 dragging = false;
891 boolean locked2 = locked;
892 locked = false;
894 if (popup) {
895 popup = false;
896 return;
899 // ban if beyond bounds:
900 if (x_p < srcRect.x || y_p < srcRect.y || x_p > srcRect.x + srcRect.width || y_p > srcRect.y + srcRect.height) {
901 return;
904 int tool = ProjectToolbar.getToolId();
906 // pan with middle mouse like in inkscape
907 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
908 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
909 tool = Toolbar.HAND;
913 switch (tool) {
914 case Toolbar.MAGNIFIER:
915 // display.updateInDatabase("srcRect"); // TODO if the display.frame
916 // is shrinked, the pack() in the zoom methods will also call the
917 // updateInDatabase("srcRect") (so it's going to be done twice)
918 display.updateFrameTitle();
919 return;
920 case Toolbar.HAND:
921 display.updateInDatabase("srcRect");
922 if (-1 != tmp_tool) {
923 ProjectToolbar.setTool(tmp_tool);
924 tmp_tool = -1;
926 if (!dragging2) repaint(true); // TEMPORARY just to allow fixing bad screen when simply cliking with the hand
927 display.getMode().srcRectUpdated(srcRect, magnification);
928 return;
931 if (input_disabled2) {
932 input_disabled2 = false; // reset
933 return;
936 if (locked2) {
937 if (ProjectToolbar.SELECT == tool) {
938 if (dragging2) {
939 Utils.showMessage("Selection is locked!");
941 return;
945 // pan with middle mouse like in inkscape
946 /* // works, but can't use it: the alt button then is useless (so no erasing with areas, and so on)
947 if (0 != (flags & InputEvent.BUTTON2_MASK)) {
948 tool = Toolbar.HAND;
952 super.flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set
953 super.flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set
954 super.flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set
956 int x_r = srcRect.x + (int)(me.getX() / magnification);
957 int y_r = srcRect.y + (int)(me.getY() / magnification);
960 if (beyond_srcRect) {
961 // Artificial release on the last dragged point
962 x_r = x_d;
963 y_r = y_d;
967 this.xMouse = x_r;
968 this.yMouse = y_r;
970 Displayable active = display.getActive();
972 switch (tool) {
973 case ProjectToolbar.PENCIL:
974 if (null != active && active.isVisible() && active.getClass() == Profile.class) {
975 if (freehandProfile == null)
976 return; // starting painting out of the DisplayCanvas boarder
977 freehandProfile.mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
978 freehandProfile = null;
979 //repaint(true);
980 Selection selection = display.getSelection();
981 selection.updateTransform(display.getActive());
982 Display.repaint(display.getLayer(), selection.getBox(), Selection.PADDING); // repaints the navigator as well
983 return;
985 break;
986 case Toolbar.RECTANGLE:
987 case Toolbar.OVAL:
988 case Toolbar.POLYGON:
989 case Toolbar.FREEROI:
990 case Toolbar.LINE:
991 case Toolbar.POLYLINE:
992 case Toolbar.FREELINE:
993 case Toolbar.ANGLE:
994 case Toolbar.POINT:
995 // pass the mouse event to superclass ImageCanvas.
996 super.mouseReleased(me);
997 repaint();
998 // return; // replaced by #SET_ROI
1001 final Roi roi = imp.getRoi();
1003 // check:
1004 if (display.isReadOnly()) return;
1006 if (tool >= ProjectToolbar.SELECT) {
1007 if (null != roi) imp.killRoi();
1008 } else return; // #SET_ROI
1010 Selection selection = display.getSelection();
1012 if (snapping) {
1013 // finish dragging
1014 display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
1015 box.add(display.getMode().getRepaintBounds());
1016 Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
1017 Display.snap((Patch)active);
1018 // reset:
1019 snapping = false;
1020 return;
1023 if (null != active && active.isVisible()) {
1024 switch(tool) {
1025 case ProjectToolbar.SELECT:
1026 display.getMode().mouseReleased(me, x_p, y_p, x_d, y_d, x_r, y_r);
1027 box.add(display.getMode().getRepaintBounds());
1028 Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
1029 break;
1030 case ProjectToolbar.PENCIL:
1031 case ProjectToolbar.PEN:
1032 case ProjectToolbar.BRUSH:
1033 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)
1034 // update active's bounding box
1035 selection.updateTransform(active);
1036 box.add(selection.getBox());
1037 Display.repaint(display.getLayer(), Selection.PADDING, box, !isTransforming(), active.isLinked() || active.getClass() == Patch.class); // does not repaint the navigator
1038 //if (!active.getClass().equals(AreaList.class)) Display.repaint(display.getLayer(), box, Selection.PADDING); // repaints the navigator as well
1039 // TODO: this last repaint call is unnecessary, if the box was properly repainted on mouse drag for Profile etc.
1040 //else
1041 if (null != old_brush_box) {
1042 repaint(old_brush_box, 0, false);
1043 old_brush_box = null; // from mouseMoved
1045 // The current state:
1046 display.getLayerSet().addDataEditStep(active);
1047 break;
1052 private boolean mouse_in = false;
1054 public void mouseEntered(MouseEvent me) {
1055 mouse_in = true;
1056 // try to catch focus if the JFrame is front most
1057 if (display.isActiveWindow() && !this.hasFocus()) {
1058 this.requestFocus();
1060 // bring dragged point to mouse pointer
1061 // TODO doesn't work as expected.
1063 Displayable active = display.getActive();
1064 int x = offScreenX(me.getX());
1065 int y = offScreenY(me.getY());
1066 if (null != active) {
1067 active.snapTo(x, y, x_p, y_p);
1068 x_p = x_d = x_d_old = x;
1069 y_p = y_d = y_d_old = y;
1072 //Utils.log2("mouseEntered x,y: " + offScreenX(me.getX()) + "," + offScreenY(me.getY()));
1075 public void mouseExited(MouseEvent me) {
1076 mouse_in = false;
1077 // paint away the circular brush if any
1078 if (ProjectToolbar.getToolId() == ProjectToolbar.BRUSH) {
1079 Displayable active = display.getActive();
1080 if (null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
1081 if (null != old_brush_box) {
1082 this.repaint(old_brush_box, 0);
1083 old_brush_box = null;
1089 /** Sets the cursor based on the current tool and cursor location. */
1090 public void setCursor(int sx, int sy, int ox, int oy) {
1091 // copy of ImageCanvas.setCursor without the win==null
1092 xMouse = ox;
1093 yMouse = oy;
1094 Roi roi = imp.getRoi();
1096 * ImageWindow win = imp.getWindow(); if (win==null) return;
1098 if (IJ.spaceBarDown()) {
1099 setCursor(handCursor);
1100 return;
1102 switch (Toolbar.getToolId()) {
1103 case Toolbar.MAGNIFIER:
1104 if (IJ.isMacintosh())
1105 setCursor(defaultCursor);
1106 else
1107 setCursor(moveCursor);
1108 break;
1109 case Toolbar.HAND:
1110 setCursor(handCursor);
1111 break;
1112 case ProjectToolbar.SELECT:
1113 case ProjectToolbar.PENCIL:
1114 setCursor(defaultCursor);
1115 break;
1116 default: // selection tool
1118 if (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.isHandle(sx, sy) >= 0)
1119 setCursor(handCursor);
1120 else if (Prefs.usePointerCursor || (roi != null && roi.getState() != Roi.CONSTRUCTING && roi.contains(ox, oy)))
1121 setCursor(defaultCursor);
1122 else
1123 setCursor(crosshairCursor);
1124 break;
1127 /** Set the srcRect - used by the DisplayNavigator. */
1128 protected void setSrcRect(int x, int y, int width, int height) {
1129 if (width < 1) width = 1;
1130 if (height < 1) height = 1;
1131 this.srcRect.setRect(x, y, width, height);
1132 display.updateInDatabase("srcRect");
1133 display.getMode().srcRectUpdated(srcRect, magnification);
1136 public void setDrawingSize(int new_width, int new_height) {
1137 adjustSrcRect(new_width, new_height);
1138 super.setDrawingSize(new_width, new_height);
1141 /** Adjust srcRect and internal variables to the canvas' bounds. */
1142 public void adjustDimensions() {
1143 final Rectangle r = getBounds();
1144 adjustSrcRect(r.width, r.height);
1145 super.dstWidth = r.width;
1146 super.dstHeight = r.height;
1149 /** Adjust srcRect to new dimensions. */
1150 public void adjustSrcRect(int new_width, int new_height) {
1151 double mag = super.getMagnification();
1152 // This method is very important! Make it fit perfectly.
1153 if (srcRect.width * mag < new_width) {
1154 // expand
1155 if (new_width > imageWidth * mag) {
1156 // too large, limit
1157 srcRect.x = 0;
1158 srcRect.width = imageWidth;
1159 } else {
1160 srcRect.width = (int) Math.ceil(new_width / mag);
1161 if (srcRect.x + srcRect.width > imageWidth) {
1162 srcRect.x = imageWidth - srcRect.width;
1165 } else {
1166 // shrink
1167 srcRect.width = (int) Math.ceil(new_width / mag);
1169 if (srcRect.height * mag < new_height) {
1170 // expand
1171 if (new_height > imageHeight * mag) {
1172 // too large, limit
1173 srcRect.y = 0;
1174 srcRect.height = imageHeight;
1175 } else {
1176 srcRect.height = (int) Math.ceil(new_height / mag);
1177 if (srcRect.y + srcRect.height > imageHeight) {
1178 srcRect.y = imageHeight - srcRect.height;
1181 } else {
1182 // shrink
1183 srcRect.height = (int) Math.ceil(new_height / mag);
1187 private void zoomIn2(int x, int y) {
1188 // copy of ImageCanvas.zoomIn except for the canEnlarge is different and
1189 // there's no call to the non-existing ImageWindow
1190 if (magnification >= 32)
1191 return;
1192 double newMag = getHigherZoomLevel2(magnification);
1194 // zoom at point: correct mag drift
1195 int cx = getWidth() / 2;
1196 int cy = getHeight() / 2;
1197 int dx = (int)(((x - cx) * magnification) / newMag);
1198 int dy = (int)(((y - cy) * magnification) / newMag);
1199 x -= dx;
1200 y -= dy;
1202 // Adjust the srcRect to the new dimensions
1203 int w = (int) Math.round(dstWidth / newMag);
1204 if (w * newMag < dstWidth)
1205 w++;
1206 if (w > imageWidth)
1207 w = imageWidth;
1208 int h = (int) Math.round(dstHeight / newMag);
1209 if (h * newMag < dstHeight)
1210 h++;
1211 if (h > imageHeight)
1212 h = imageHeight;
1213 x = offScreenX(x);
1214 y = offScreenY(y);
1215 final Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
1216 if (r.x < 0)
1217 r.x = 0;
1218 if (r.y < 0)
1219 r.y = 0;
1220 if (r.x + w > imageWidth)
1221 r.x = imageWidth - w;
1222 if (r.y + h > imageHeight)
1223 r.y = imageHeight - h;
1224 srcRect = r;
1226 //display.pack();
1228 setMagnification(newMag);
1229 display.updateInDatabase("srcRect");
1230 display.repaintAll2(); // this repaint includes this canvas's repaint as well, but also the navigator, etc. // repaint();
1233 private void zoomOut2(int x, int y) {
1234 //if (magnification <= 0.03125)
1235 // return;
1236 double newMag = getLowerZoomLevel2(magnification);
1238 // zoom at point: correct mag drift
1239 int cx = getWidth() / 2;
1240 int cy = getHeight() / 2;
1241 int dx = (int)(((x - cx) * magnification) / newMag);
1242 int dy = (int)(((y - cy) * magnification) / newMag);
1243 x -= dx;
1244 y -= dy;
1246 if (imageWidth * newMag > dstWidth || imageHeight * newMag > dstHeight) {
1247 int w = (int) Math.round(dstWidth / newMag);
1248 if (w * newMag < dstWidth)
1249 w++;
1250 int h = (int) Math.round(dstHeight / newMag);
1251 if (h * newMag < dstHeight)
1252 h++;
1253 x = offScreenX(x);
1254 y = offScreenY(y);
1255 Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
1256 if (r.x < 0)
1257 r.x = 0;
1258 if (r.y < 0)
1259 r.y = 0;
1260 if (r.x + w > imageWidth)
1261 r.x = imageWidth - w;
1262 if (r.y + h > imageHeight)
1263 r.y = imageHeight - h;
1264 srcRect = r;
1265 } else {
1266 // Shrink srcRect, but NOT the dstWidth,dstHeight of the canvas, which remain the same:
1267 srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
1270 setMagnification(newMag);
1271 display.repaintAll2(); // this repaint includes this canvas's repaint, but updates the navigator without update_graphics
1272 display.updateInDatabase("srcRect");
1275 /** The minimum amout of pixels allowed for width or height when zooming out. */
1276 static private final int MIN_DIMENSION = 10; // pixels
1278 /** Enable zooming out up to the point where the display becomes 10 pixels in width or height. */
1279 protected double getLowerZoomLevel2(final double currentMag) {
1280 // if it is 1/72 or lower, then:
1281 if (Math.abs(currentMag - 1/72.0) < 0.00000001 || currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
1282 // find nearest power of two under currentMag
1283 // start at level 7, which is 1/128
1284 int level = 7;
1285 double scale = currentMag;
1286 while (scale * srcRect.width > MIN_DIMENSION && scale * srcRect.height > MIN_DIMENSION) {
1287 scale = 1 / Math.pow(2, level);
1288 // if not equal and actually smaller, break:
1289 if (Math.abs(scale - currentMag) != 0.00000001 && scale < currentMag) break;
1290 level++;
1292 return scale;
1293 } else {
1294 return ImageCanvas.getLowerZoomLevel(currentMag);
1297 protected double getHigherZoomLevel2(final double currentMag) {
1298 // if it is not 1/72 and its lower, then:
1299 if (Math.abs(currentMag - 1/72.0) > 0.00000001 && currentMag < 1/72.0) { // lowest zoomLevel in ImageCanvas is 1/72.0
1300 // find nearest power of two above currentMag
1301 // start at level 14, which is 0.00006103515625 (0.006 %)
1302 int level = 14; // this value may be increased in the future
1303 double scale = currentMag;
1304 while (level >= 0) {
1305 scale = 1 / Math.pow(2, level);
1306 if (scale > currentMag) break;
1307 level--;
1309 return scale;
1310 } else {
1311 return ImageCanvas.getHigherZoomLevel(currentMag);
1317 * // OBSOLETE: modified ij.gui.ImageCanvas directly
1318 public void mouseMoved(MouseEvent e) { if (IJ.getInstance()==null) return; int sx =
1319 * e.getX(); int sy = e.getY(); int ox = offScreenX(sx); int oy =
1320 * offScreenY(sy); flags = e.getModifiers(); setCursor(sx, sy, ox, oy);
1321 * IJ.setInputEvent(e); Roi roi = imp.getRoi(); if (roi!=null &&
1322 * (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE ||
1323 * roi.getType()==Roi.ANGLE) && roi.getState()==roi.CONSTRUCTING) {
1324 * PolygonRoi pRoi = (PolygonRoi)roi; pRoi.handleMouseMove(ox, oy); } else {
1325 * if (ox<imageWidth && oy<imageHeight) { //ImageWindow win =
1326 * imp.getWindow(); //if (win!=null) win.mouseMoved(ox, oy);
1327 * imp.mouseMoved(ox, oy); } else IJ.showStatus(""); } }
1330 private Rectangle old_brush_box = null;
1332 private MouseMovedThread mouse_moved = new MouseMovedThread();
1334 private class MouseMovedThread extends Thread {
1335 private volatile MouseEvent me = null;
1336 MouseMovedThread() {
1337 super("T2-mouseMoved");
1338 setDaemon(true);
1339 setPriority(Thread.NORM_PRIORITY);
1340 start();
1342 void dispatch(MouseEvent me) {
1343 //Utils.log2("before");
1344 synchronized (this) {
1345 //Utils.log2("in");
1346 this.me = me;
1347 notifyAll();
1350 void quit() {
1351 interrupt();
1352 synchronized (this) { notifyAll(); }
1354 public void run() {
1355 while (!isInterrupted()) {
1356 MouseEvent me = this.me;
1357 if (null != me) {
1358 try { mouseMoved(me); } catch (Exception e) { IJError.print(e); }
1360 // Wait only if the event has not changed
1361 synchronized (this) {
1362 if (me == this.me) {
1363 // Release the pointer
1364 me = null;
1365 this.me = null;
1366 if (isInterrupted()) return;
1367 // Wait until there is a new event
1368 try { wait(); } catch (Exception e) {}
1373 private void mouseMoved(MouseEvent me) {
1374 if (null == me) return;
1376 if (input_disabled || display.getMode().isDragging()) return;
1378 xMouse = (int)(me.getX() / magnification) + srcRect.x;
1379 yMouse = (int)(me.getY() / magnification) + srcRect.y;
1380 final Displayable active = display.getActive();
1382 // only when no mouse buttons are down
1383 final int flags = DisplayCanvas.super.flags;
1384 if (0 == (flags & InputEvent.BUTTON1_MASK)
1385 /* && 0 == (flags & InputEvent.BUTTON2_MASK) */ // this is the alt key down ..
1386 && 0 == (flags & InputEvent.BUTTON3_MASK)
1387 //if (me.getButton() == MouseEvent.NOBUTTON
1388 && null != active && active.isVisible() && AreaContainer.class.isInstance(active)) {
1389 final int tool = ProjectToolbar.getToolId();
1390 Rectangle r = null;
1391 if (ProjectToolbar.BRUSH == tool) {
1392 // repaint area where the brush circle is
1393 int brushSize = ProjectToolbar.getBrushSize() +2; // +2 padding
1394 r = new Rectangle( xMouse - brushSize/2,
1395 yMouse - brushSize/2,
1396 brushSize+1,
1397 brushSize+1 );
1398 } else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
1399 // repaint area where the fast-marching box is
1400 r = new Rectangle( xMouse - Segmentation.fmp.width/2 - 2,
1401 yMouse - Segmentation.fmp.height/2 - 2,
1402 Segmentation.fmp.width + 4,
1403 Segmentation.fmp.height + 4 );
1405 if (null != r) {
1406 Rectangle copy = (Rectangle)r.clone();
1407 if (null != old_brush_box) r.add(old_brush_box);
1408 old_brush_box = copy;
1409 repaint(r, 1); // padding because of painting rounding which would live dirty trails
1413 if (me.isShiftDown()) {
1414 // Print a comma-separated list of objects under the mouse pointer
1415 final Layer layer = DisplayCanvas.this.display.getLayer();
1416 final List<Displayable> al = getDisplayablesUnderMouse(me);
1417 if (0 == al.size()) {
1418 Utils.showStatus("", false);
1419 return;
1421 final StringBuilder sb = new StringBuilder();
1422 final Project pr = layer.getProject();
1423 for (Displayable d : al) sb.append(pr.getShortMeaningfulTitle(d)).append(", ");
1424 sb.setLength(sb.length()-2);
1425 Utils.showStatus(sb.toString(), false);
1426 } else {
1427 // For very large images, the Patch.getPixel can take even half a minute
1428 // to do the pixel grab operation.
1429 //DisplayCanvas.super.mouseMoved(me);
1430 // Instead, find out over what are we
1431 final List<Displayable> under = getDisplayablesUnderMouse(me);
1432 final Calibration cal = display.getLayerSet().getCalibration();
1433 if (under.isEmpty()) {
1434 Utils.showStatus("x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
1435 + ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit());
1436 return;
1438 final Displayable top = under.get(0);
1439 String msg =
1440 "x=" + (int)(xMouse * cal.pixelWidth) + " " + cal.getUnit()
1441 + ", y=" + (int)(yMouse * cal.pixelHeight) + " " + cal.getUnit();
1442 if (top.getClass() == Patch.class) {
1443 final Patch patch = (Patch)top;
1444 final int[] p = new int[4];
1445 BufferedImage offsc;
1446 synchronized (offscreen_lock) {
1447 offsc = offscreen;
1449 if (null == offsc) return;
1450 try {
1451 PixelGrabber pg = new PixelGrabber(offsc, me.getX(), me.getY(), 1, 1, p, 0, offsc.getWidth(null));
1452 pg.grabPixels();
1453 } catch (InterruptedException ie) {
1454 IJError.print(ie);
1455 return;
1456 } catch (Throwable t) {
1457 // The offscreen might have been flushed. Just ignore; pixel value will be reported next.
1458 return;
1460 patch.approximateTransferPixel(p);
1461 msg += ", value=";
1462 switch (patch.getType()) {
1463 case ImagePlus.GRAY16:
1464 case ImagePlus.GRAY8:
1465 msg += p[0];
1466 break;
1467 case ImagePlus.GRAY32:
1468 msg += Float.intBitsToFloat(p[0]);
1469 break;
1470 case ImagePlus.COLOR_RGB:
1471 case ImagePlus.COLOR_256:
1472 msg += "(" + p[0] + "," + p[1] + "," + p[2] + ")";
1473 break;
1475 msg += " [Patch #" + patch.getId() + "]";
1476 } else {
1477 final Color c = top.getColor();
1478 msg += ", value=[" + c.getRed() + "," + c.getGreen() + "," + c.getBlue() + "] [" + Project.getName(top.getClass()) + " #" + top.getId() + "]";
1480 Utils.showStatus(msg);
1485 /** See {@link DisplayCanvas#getDisplayablesUnderMouse(MouseEvent)}. */
1486 public List<Displayable> getDisplayablesUnderMouse() {
1487 return getDisplayablesUnderMouse(new MouseEvent(this, -1, 0, 0, xMouse, yMouse, 1, false));
1490 /** Return the list of Displayable objects under the mouse,
1491 * sorted by proper stack order. */
1492 public List<Displayable> getDisplayablesUnderMouse(MouseEvent me) {
1493 final Layer layer = display.getLayer();
1494 final int x_p = offScreenX(me.getX()),
1495 y_p = offScreenY(me.getY());
1496 final ArrayList<Displayable> al = new ArrayList<Displayable>(layer.getParent().findZDisplayables(layer, x_p, y_p, true));
1497 Collections.reverse(al);
1498 final ArrayList<Displayable> al2 = new ArrayList<Displayable>(layer.find(x_p, y_p, true));
1499 Collections.reverse(al2);
1500 al.addAll(al2);
1501 return al;
1504 public boolean isDragging() {
1505 return display.getMode().isDragging();
1508 public void mouseMoved(final MouseEvent me) {
1509 super.flags = me.getModifiers();
1510 mouse_moved.dispatch(me);
1513 /** Zoom in using the current mouse position, or the center if the mouse is out. */
1514 public void zoomIn() {
1515 if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
1516 zoomIn(dstWidth/2, dstHeight/2);
1517 } else {
1518 zoomIn(screenX(xMouse), screenY(yMouse));
1522 /** Overriding to repaint the DisplayNavigator as well. */
1523 public void zoomIn(int x, int y) {
1524 update_graphics = true; // update the offscreen images.
1525 zoomIn2(x, y);
1528 /** Zoom out using the current mouse position, or the center if the mouse is out. */
1529 public void zoomOut() {
1530 if (xMouse < 0 || screenX(xMouse) > dstWidth || yMouse < 0 || screenY(yMouse) > dstHeight) {
1531 zoomOut(dstWidth/2, dstHeight/2);
1532 } else zoomOut(screenX(xMouse), screenY(yMouse));
1535 /** Overriding to repaint the DisplayNavigator as well. */
1536 public void zoomOut(int x, int y) {
1537 update_graphics = true; // update the offscreen images.
1538 zoomOut2(x, y);
1541 /** Center the srcRect around the given object(s) bounding box, zooming if necessary,
1542 * so that the given r becomes a rectangle centered in the srcRect and zoomed out by a factor of 2. */
1543 public void showCentered(Rectangle r) {
1544 // multiply bounding box dimensions by two
1545 r.x -= r.width / 2;
1546 r.y -= r.height / 2;
1547 r.width += r.width;
1548 r.height += r.height;
1549 // compute target magnification
1550 double magn = getWidth() / (double)(r.width > r.height ? r.width : r.height);
1551 center(r, magn);
1554 /** Show the given r as the srcRect (or as much of it as possible) at the given magnification. */
1555 public void center(Rectangle r, double magn) {
1556 // bring bounds within limits of the layer and the canvas' drawing size
1557 double lw = display.getLayer().getLayerWidth();
1558 double lh = display.getLayer().getLayerHeight();
1559 int cw = (int) (getWidth() / magn); // canvas dimensions in offscreen coords
1560 int ch = (int) (getHeight() / magn);
1562 // 2nd attempt:
1563 // fit to canvas drawing size:
1564 r.y += (r.height - ch) / 2;
1565 r.width = cw;
1566 r.height = ch;
1567 // place within layer bounds
1568 if (r.x < 0) r.x = 0;
1569 if (r.y < 0) r.y = 0;
1570 if (r.width > lw) {
1571 r.x = 0;
1572 r.width = (int)lw;
1574 if (r.height > lh) {
1575 r.y = 0;
1576 r.height = (int)lh;
1578 if (r.x + r.width > lw) r.x = (int)(lw - cw);
1579 if (r.y + r.height > lh) r.y = (int)(lh - ch);
1580 // compute magn again, since the desired width may have changed:
1581 magn = getWidth() / (double)r.width;
1583 // set magnification and srcRect
1584 setup(magn, r);
1585 try { Thread.sleep(200); } catch (Exception e) {} // swing ... waiting for the display.pack()
1586 update_graphics = true;
1587 RT.paint(null, update_graphics);
1588 display.updateInDatabase("srcRect");
1589 display.updateFrameTitle();
1590 display.getNavigator().repaint(false);
1593 /** 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. */
1594 public void repaint(Displayable d) {
1595 repaint(d, 0);
1599 * Repaint as much as the bounding box around the given Displayable plus the
1600 * extra padding. If the Displayable is null, the entire canvas is
1601 * repainted, remaking the offscreen images.
1603 public void repaint(Displayable displ, int extra) {
1604 repaint(displ, extra, update_graphics);
1606 public void repaint(Displayable displ, int extra, boolean update_graphics) {
1607 if (null != displ) {
1608 Rectangle r = displ.getBoundingBox();
1609 r.x = (int) ((r.x - srcRect.x) * magnification) - extra;
1610 r.y = (int) ((r.y - srcRect.y) * magnification) - extra;
1611 r.width = (int) Math.ceil(r.width * magnification) + extra + extra;
1612 r.height = (int) Math.ceil(r.height * magnification) + extra + extra;
1613 invalidateVolatile();
1614 RT.paint(r, update_graphics);
1615 } else {
1616 // everything
1617 repaint(true);
1622 * Repaint the clip corresponding to the sum of all boundingboxes of
1623 * Displayable objects in the hashset.
1625 // it is assumed that the linked objects are close to each other, otherwise
1626 // the clip rectangle grows enormously.
1627 public void repaint(final HashSet<Displayable> hs) {
1628 if (null == hs) return;
1629 Rectangle r = null;
1630 final Layer dl = display.getLayer();
1631 for (final Displayable d : hs) {
1632 if (d.getLayer() == dl) {
1633 if (null == r) r = d.getBoundingBox();
1634 else r.add(d.getBoundingBox());
1637 if (null != r) {
1638 //repaint(r.x, r.y, r.width, r.height);
1639 invalidateVolatile();
1640 RT.paint(r, update_graphics);
1645 * Repaint the given offscreen Rectangle after transforming its data on the fly to the
1646 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
1647 * modified.
1649 public void repaint(final Rectangle r, final int extra) {
1650 invalidateVolatile();
1651 if (null == r) {
1652 //Utils.log2("DisplayCanvas.repaint(Rectangle, int) warning: null r");
1653 RT.paint(null, update_graphics);
1654 return;
1656 // 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);
1657 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);
1661 * Repaint the given Rectangle after transforming its data on the fly to the
1662 * srcRect and magnification of this DisplayCanvas. The Rectangle is not
1663 * modified.
1664 * @param box The rectangle to repaint
1665 * @param extra The extra outbound padding to add to the rectangle
1666 * @param update_graphics Whether to recreate the offscreen images or not
1668 public void repaint(Rectangle box, int extra, boolean update_graphics) {
1669 this.update_graphics = update_graphics;
1670 repaint(box, extra);
1673 /** Repaint everything, updating offscreen graphics if so specified. */
1674 public void repaint(final boolean update_graphics) {
1675 this.update_graphics = update_graphics | this.update_graphics;
1676 invalidateVolatile();
1677 RT.paint(null, this.update_graphics);
1680 /** Overridden to multithread. This method is here basically to enable calls to the FakeImagePlus.draw from the HAND and other tools to repaint properly.*/
1681 public void repaint() {
1682 //Utils.log2("issuing thread");
1683 invalidateVolatile();
1684 RT.paint(null, update_graphics);
1687 /** Overridden to multithread. */
1688 /* // saved as unoveridden to make sure there are no infinite thread loops when calling super in buggy JVMs
1689 public void repaint(long ms, int x, int y, int width, int height) {
1690 RT.paint(new Rectangle(x, y, width, height), update_graphics);
1694 /** Overridden to multithread. */
1695 public void repaint(int x, int y, int width, int height) {
1696 invalidateVolatile();
1697 RT.paint(new Rectangle(x, y, width, height), update_graphics);
1700 public void setUpdateGraphics(boolean b) {
1701 update_graphics = b;
1704 /** Release offscreen images and stop threads. */
1705 public void flush() {
1706 // cleanup update graphics thread if any
1707 RT.quit();
1708 synchronized (offscreen_lock) {
1709 if (null != offscreen) {
1710 offscreen.flush();
1711 offscreen = null;
1713 update_graphics = true;
1714 for (final BufferedImage bi : to_flush) bi.flush();
1715 to_flush.clear();
1717 mouse_moved.quit();
1718 try {
1719 synchronized (this) { if (null != animator) animator.shutdownNow(); }
1720 cancelAnimations();
1721 } catch (Exception e) {}
1722 animator = null;
1725 public void destroy() {
1726 flush();
1727 WindowManager.setTempCurrentImage(imp); // the FakeImagePlus
1728 WindowManager.removeWindow(fake_win); // the FakeImageWindow
1731 public boolean applyTransform() {
1732 boolean b = display.getMode().apply();
1733 if (b) {
1734 display.setMode(new DefaultMode(display));
1735 Display.repaint();
1737 return b;
1740 public boolean isTransforming() {
1741 // TODO this may have to change if modes start getting used for a task other than transformation.
1742 // Perhaps "isTransforming" will have to broaden its meaning to "isNotDefaultMode"
1743 return display.getMode().getClass() != DefaultMode.class;
1746 public void cancelTransform() {
1747 display.getMode().cancel();
1748 display.setMode(new DefaultMode(display));
1749 repaint(true);
1752 private int last_keyCode = KeyEvent.VK_ESCAPE;
1753 private boolean tagging = false;
1755 public void keyPressed(KeyEvent ke) {
1757 Displayable active = display.getActive();
1759 if (null != freehandProfile
1760 && ProjectToolbar.getToolId() == ProjectToolbar.PENCIL
1761 && ke.getKeyCode() == KeyEvent.VK_ESCAPE
1762 && null != freehandProfile)
1764 freehandProfile.abort();
1765 ke.consume();
1766 return;
1769 final int keyCode = ke.getKeyCode();
1771 try {
1772 // Enable tagging system for any alphanumeric key:
1773 if (!input_disabled && null != active && active instanceof Tree<?> && ProjectToolbar.isDataEditTool(ProjectToolbar.getToolId())) {
1774 if (tagging) {
1775 if (KeyEvent.VK_0 == keyCode && KeyEvent.VK_0 != last_keyCode) {
1776 // do nothing: keep tagging as true
1777 } else {
1778 // last step of tagging: a char after t or after t and a number (and the char itself can be a number)
1779 tagging = false;
1781 active.keyPressed(ke);
1782 return;
1783 } else if (KeyEvent.VK_T == keyCode) {
1784 tagging = true;
1785 active.keyPressed(ke);
1786 return;
1789 } finally {
1790 last_keyCode = keyCode;
1793 tagging = false;
1795 if (ke.isConsumed()) return;
1798 * TODO screen editor ... TEMPORARY if (active instanceof DLabel) {
1799 * active.keyPressed(ke); ke.consume(); return; }
1802 if (!zoom_and_pan) {
1803 if (KeyEvent.VK_ESCAPE == keyCode) {
1804 cancelAnimations();
1806 return;
1809 final int keyChar = ke.getKeyChar();
1811 boolean used = false;
1813 switch (keyChar) {
1814 case '+':
1815 case '=':
1816 zoomIn();
1817 used = true;
1818 break;
1819 case '-':
1820 case '_':
1821 zoomOut();
1822 used = true;
1823 break;
1824 default:
1825 break;
1829 if (used) {
1830 ke.consume(); // otherwise ImageJ would use it!
1831 return;
1834 if (input_disabled) {
1835 if (KeyEvent.VK_ESCAPE == keyCode) {
1836 // cancel last job if any
1837 if (Utils.checkYN("Really cancel job?")) {
1838 display.getProject().getLoader().quitJob(null);
1839 display.repairGUI();
1842 ke.consume();
1843 return; // only zoom is enabled, above
1846 if (KeyEvent.VK_S == keyCode && 0 == ke.getModifiers() && display.getProject().getLoader().isAsynchronous()) {
1847 display.getProject().getLoader().saveTask(display.getProject(), "Save");
1848 ke.consume();
1849 return;
1850 } else if (KeyEvent.VK_F == keyCode && Utils.isControlDown(ke)) {
1851 Search.showWindow();
1852 ke.consume();
1853 return;
1856 // if display is not read-only, check for other keys:
1857 switch (keyChar) {
1858 case '<':
1859 case ',': // select next Layer up
1860 display.previousLayer(ke.getModifiers()); // repaints as well
1861 ke.consume();
1862 return;
1863 case '>':
1864 case '.': // select next Layer down
1865 display.nextLayer(ke.getModifiers());
1866 ke.consume();
1867 return;
1870 if (null == active && null != imp.getRoi() && KeyEvent.VK_A != keyCode) { // control+a and a roi should select under roi
1871 IJ.getInstance().keyPressed(ke);
1872 return;
1875 // end here if display is read-only
1876 if (display.isReadOnly()) {
1877 ke.consume();
1878 display.repaintAll();
1879 return;
1882 if (KeyEvent.VK_ENTER == keyCode) {
1883 if (isTransforming()) {
1884 applyTransform();
1885 ke.consume();
1886 return;
1887 } else {
1888 IJ.getInstance().toFront();
1889 ke.consume();
1890 return;
1894 // check preconditions (or the keys are meaningless). Allow 'enter' to
1895 // bring forward the ImageJ window, and 'v' to paste a patch.
1896 /*if (null == active && KeyEvent.VK_ENTER != keyCode && KeyEvent.VK_V != keyCode && KeyEvent) {
1897 return;
1900 Layer layer = display.getLayer();
1902 final int mod = ke.getModifiers();
1904 switch (keyCode) {
1905 case KeyEvent.VK_COMMA:
1906 case 0xbc: // select next Layer up
1907 display.nextLayer(ke.getModifiers());
1908 break;
1909 case KeyEvent.VK_PERIOD:
1910 case 0xbe: // select next Layer down
1911 display.previousLayer(ke.getModifiers());
1912 break;
1913 case KeyEvent.VK_Z:
1914 // UNDO: shift+z or ctrl+z
1915 if (0 == (mod ^ Event.SHIFT_MASK) || 0 == (mod ^ Utils.getControlModifier())) {
1916 Bureaucrat.createAndStart(new Worker.Task("Undo") { public void exec() {
1917 if (isTransforming()) display.getMode().undoOneStep();
1918 else display.getLayerSet().undoOneStep();
1919 Display.repaint(display.getLayerSet());
1920 }}, display.getProject());
1921 ke.consume();
1922 // REDO: alt+z or ctrl+shift+z
1923 } else if (0 == (mod ^ Event.ALT_MASK) || 0 == (mod ^ (Event.SHIFT_MASK | Utils.getControlModifier())) ) {
1924 Bureaucrat.createAndStart(new Worker.Task("Redo") { public void exec() {
1925 if (isTransforming()) display.getMode().redoOneStep();
1926 else display.getLayerSet().redoOneStep();
1927 Display.repaint(display.getLayerSet());
1928 }}, display.getProject());
1929 ke.consume();
1931 // else, the 'z' command restores the image using ImageJ internal undo
1932 break;
1933 case KeyEvent.VK_T:
1934 // Enable with any tool to the left of the PENCIL
1935 if (null != active && !isTransforming() && ProjectToolbar.getToolId() < ProjectToolbar.PENCIL) {
1936 ProjectToolbar.setTool(ProjectToolbar.SELECT);
1937 if (0 == ke.getModifiers()) {
1938 display.setMode(new AffineTransformMode(display));
1939 } else if (Event.SHIFT_MASK == ke.getModifiers()) {
1940 for (final Displayable d : display.getSelection().getSelected()) {
1941 if (d.isLinked()) {
1942 Utils.showMessage("Can't enter manual non-linear transformation mode:\nat least one image is linked.");
1943 return;
1946 display.setMode(new NonLinearTransformMode(display));
1948 ke.consume();
1950 // else, let ImageJ grab the ROI into the Manager, if any
1951 break;
1952 case KeyEvent.VK_A:
1953 if (0 == (ke.getModifiers() ^ Utils.getControlModifier())) {
1954 Roi roi = getFakeImagePlus().getRoi();
1955 if (null != roi) display.getSelection().selectAll(roi, true);
1956 else display.getSelection().selectAllVisible();
1957 Display.repaint(display.getLayer(), display.getSelection().getBox(), 0);
1958 ke.consume();
1959 break; // INSIDE the 'if' block, so that it can bleed to the default block which forwards to active!
1960 } else if (null != active) {
1961 active.keyPressed(ke);
1962 if (ke.isConsumed()) break;
1963 // TODO this is just a hack really. Should just fall back to default switch option.
1964 // The whole keyPressed method needs revision: should not break from it when not using the key.
1966 break;
1967 case KeyEvent.VK_ESCAPE: // cancel transformation
1968 if (isTransforming()) cancelTransform();
1969 else {
1970 display.select(null); // deselect
1971 // repaint out the brush if present
1972 if (ProjectToolbar.BRUSH == ProjectToolbar.getToolId()) {
1973 repaint(old_brush_box, 0);
1976 ke.consume();
1977 break;
1978 case KeyEvent.VK_SPACE:
1979 if (0 == ke.getModifiers()) {
1980 if (null != active) {
1981 invalidateVolatile();
1982 if (Math.abs(active.getAlpha() - 0.5f) > 0.001f) active.setAlpha(0.5f);
1983 else active.setAlpha(1.0f);
1984 display.setTransparencySlider(active.getAlpha());
1985 Display.repaint();
1986 ke.consume();
1988 } else {
1989 // ;)
1990 int kem = ke.getModifiers();
1991 if (0 != (kem & KeyEvent.SHIFT_MASK)
1992 && 0 != (kem & KeyEvent.ALT_MASK)
1993 && 0 != (kem & KeyEvent.CTRL_MASK)) {
1994 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.");
1995 ke.consume();
1998 break;
1999 case KeyEvent.VK_S:
2000 if (ke.isAltDown()) {
2001 snapping = true;
2002 ke.consume();
2003 } else if (dragging) {
2004 // 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)
2005 ke.consume();
2007 break;
2008 case KeyEvent.VK_H:
2009 handleHide(ke);
2010 ke.consume();
2011 break;
2012 case KeyEvent.VK_J:
2013 if (!display.getSelection().isEmpty()) {
2014 display.adjustMinAndMaxGUI();
2015 ke.consume();
2017 break;
2018 case KeyEvent.VK_F1:
2019 case KeyEvent.VK_F2:
2020 case KeyEvent.VK_F3:
2021 case KeyEvent.VK_F4:
2022 case KeyEvent.VK_F5:
2023 case KeyEvent.VK_F6:
2024 case KeyEvent.VK_F7:
2025 case KeyEvent.VK_F8:
2026 case KeyEvent.VK_F9:
2027 case KeyEvent.VK_F10:
2028 case KeyEvent.VK_F11:
2029 case KeyEvent.VK_F12:
2030 ProjectToolbar.keyPressed(ke);
2031 ke.consume();
2032 break;
2033 case KeyEvent.VK_M:
2034 if (0 == ke.getModifiers() && ProjectToolbar.getToolId() == ProjectToolbar.SELECT) {
2035 display.getSelection().measure();
2036 ke.consume();
2038 break;
2041 switch (keyChar) {
2042 case ':':
2043 case ';':
2044 if (null != active && active instanceof ZDisplayable) {
2045 if (null != display.getProject().getProjectTree().tryAddNewConnector(active, true)) {
2046 ProjectToolbar.setTool(ProjectToolbar.PEN);
2048 ke.consume();
2050 break;
2053 if (ke.isConsumed()) return;
2055 if (null != active) {
2056 if (display.getMode().getClass() == DefaultMode.class) {
2057 active.keyPressed(ke);
2059 if (ke.isConsumed()) return;
2062 // Else:
2063 switch (keyCode) {
2064 case KeyEvent.VK_G:
2065 if (browseToNodeLayer(ke.isShiftDown())) {
2066 ke.consume();
2068 break;
2069 case KeyEvent.VK_I:
2070 if (ke.isAltDown()) {
2071 if (ke.isShiftDown()) display.importImage();
2072 else display.importNextImage();
2073 ke.consume();
2075 break;
2076 case KeyEvent.VK_PAGE_UP: // as in Inkscape
2077 if (null != active) {
2078 update_graphics = true;
2079 layer.getParent().addUndoMoveStep(active);
2080 layer.getParent().move(LayerSet.UP, active);
2081 layer.getParent().addUndoMoveStep(active);
2082 Display.repaint(layer, active, 5);
2083 Display.updatePanelIndex(layer, active);
2084 ke.consume();
2086 break;
2087 case KeyEvent.VK_PAGE_DOWN: // as in Inkscape
2088 if (null != active) {
2089 update_graphics = true;
2090 layer.getParent().addUndoMoveStep(active);
2091 layer.getParent().move(LayerSet.DOWN, active);
2092 layer.getParent().addUndoMoveStep(active);
2093 Display.repaint(layer, active, 5);
2094 Display.updatePanelIndex(layer, active);
2095 ke.consume();
2097 break;
2098 case KeyEvent.VK_HOME: // as in Inkscape
2099 if (null != active) {
2100 update_graphics = true;
2101 layer.getParent().addUndoMoveStep(active);
2102 layer.getParent().move(LayerSet.TOP, active);
2103 layer.getParent().addUndoMoveStep(active);
2104 Display.repaint(layer, active, 5);
2105 Display.updatePanelIndex(layer, active);
2106 ke.consume();
2108 break;
2109 case KeyEvent.VK_END: // as in Inkscape
2110 if (null != active) {
2111 update_graphics = true;
2112 layer.getParent().addUndoMoveStep(active);
2113 layer.getParent().move(LayerSet.BOTTOM, active);
2114 layer.getParent().addUndoMoveStep(active);
2115 Display.repaint(layer, active, 5);
2116 Display.updatePanelIndex(layer, active);
2117 ke.consume();
2119 break;
2120 case KeyEvent.VK_V:
2121 if (0 == ke.getModifiers()) {
2122 if (null == active || active.getClass() == Patch.class) {
2123 // paste a new image
2124 ImagePlus clipboard = ImagePlus.getClipboard();
2125 if (null != clipboard) {
2126 ImagePlus imp = new ImagePlus(clipboard.getTitle() + "_" + System.currentTimeMillis(), clipboard.getProcessor().crop());
2127 Object info = clipboard.getProperty("Info");
2128 if (null != info) imp.setProperty("Info", (String)info);
2129 double x = srcRect.x + srcRect.width/2 - imp.getWidth()/2;
2130 double y = srcRect.y + srcRect.height/2 - imp.getHeight()/2;
2131 // save the image somewhere:
2132 Patch pa = display.getProject().getLoader().addNewImage(imp, x, y);
2133 display.getLayer().add(pa);
2134 ke.consume();
2135 } // TODO there isn't much ImageJ integration in the pasting. Can't paste to a selected image, for example.
2136 } else {
2137 // Each type may know how to paste data from the copy buffer into itself:
2138 active.keyPressed(ke);
2139 ke.consume();
2142 break;
2143 case KeyEvent.VK_P:
2144 if (0 == ke.getModifiers()) {
2145 display.getLayerSet().color_cues = !display.getLayerSet().color_cues;
2146 Display.repaint(display.getLayerSet());
2147 ke.consume();
2149 break;
2150 case KeyEvent.VK_F:
2151 if (0 == (ke.getModifiers() ^ KeyEvent.SHIFT_MASK)) {
2152 // toggle visibility of tags
2153 display.getLayerSet().paint_tags = !display.getLayerSet().paint_tags;
2154 Display.repaint();
2155 ke.consume();
2156 } else if (0 == (ke.getModifiers() ^ KeyEvent.ALT_MASK)) {
2157 // toggle visibility of edge arrows
2158 display.getLayerSet().paint_arrows = !display.getLayerSet().paint_arrows;
2159 Display.repaint();
2160 ke.consume();
2161 } else if (0 == ke.getModifiers()) {
2162 // toggle visibility of edge confidence boxes
2163 display.getLayerSet().paint_edge_confidence_boxes = !display.getLayerSet().paint_edge_confidence_boxes;
2164 Display.repaint();
2165 ke.consume();
2167 break;
2168 case KeyEvent.VK_DELETE:
2169 if (0 == ke.getModifiers()) {
2170 display.getSelection().deleteAll();
2172 break;
2173 case KeyEvent.VK_B:
2174 if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
2175 display.duplicateLinkAndSendTo(active, 0, active.getLayer().getParent().previous(layer));
2176 ke.consume();
2178 break;
2179 case KeyEvent.VK_N:
2180 if (0 == ke.getModifiers() && null != active && active.getClass() == Profile.class) {
2181 display.duplicateLinkAndSendTo(active, 1, active.getLayer().getParent().next(layer));
2182 ke.consume();
2184 break;
2185 case KeyEvent.VK_1:
2186 case KeyEvent.VK_2:
2187 case KeyEvent.VK_3:
2188 case KeyEvent.VK_4:
2189 case KeyEvent.VK_5:
2190 case KeyEvent.VK_6:
2191 case KeyEvent.VK_7:
2192 case KeyEvent.VK_8:
2193 case KeyEvent.VK_9:
2194 // run a plugin, if any
2195 if (null != Utils.launchTPlugIn(ke, "Display", display.getProject(), display.getActive())) {
2196 ke.consume();
2197 break;
2201 if ( !(keyCode == KeyEvent.VK_UNDEFINED || keyChar == KeyEvent.CHAR_UNDEFINED) && !ke.isConsumed() && null != active && active instanceof Patch) {
2202 // TODO should allow forwarding for all, not just Patch
2203 // forward to ImageJ for a final try
2204 IJ.getInstance().keyPressed(ke);
2205 repaint(active, 5);
2206 ke.consume();
2208 //Utils.log2("keyCode, keyChar: " + keyCode + ", " + keyChar + " ref: " + KeyEvent.VK_UNDEFINED + ", " + KeyEvent.CHAR_UNDEFINED);
2211 public void keyTyped(KeyEvent ke) {}
2213 public void keyReleased(KeyEvent ke) {}
2215 public void zoomToFit() {
2216 double magw = (double) getWidth() / imageWidth;
2217 double magh = (double) getHeight() / imageHeight;
2218 this.magnification = magw < magh ? magw : magh;
2219 this.srcRect.setRect(0, 0, imageWidth, imageHeight);
2220 setMagnification(magnification);
2221 display.updateInDatabase("srcRect"); // includes magnification
2225 public void setReceivesInput(boolean b) {
2226 this.input_disabled = !b;
2229 public boolean isInputEnabled() {
2230 return !input_disabled;
2233 /** 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.*/
2234 public ImagePlus getFakeImagePlus() {
2235 return this.imp;
2238 /** Key/Mouse bindings like:
2239 * - ij.gui.StackWindow: wheel to scroll slices (in this case Layers)
2240 * - Inkscape: control+wheel to zoom (apple+wheel in macosx, since control+wheel zooms desktop)
2242 public void mouseWheelMoved(MouseWheelEvent mwe) {
2243 if (dragging) return; // prevent unexpected mouse wheel movements
2244 final int modifiers = mwe.getModifiers();
2245 final int rotation = mwe.getWheelRotation();
2246 final int tool = ProjectToolbar.getToolId();
2247 if (0 != (modifiers & Utils.getControlModifier())) {
2248 if (!zoom_and_pan) return;
2249 // scroll zoom under pointer
2250 int x = mwe.getX();
2251 int y = mwe.getY();
2252 if (x < 0 || y < 0 || x >= getWidth() || y >= getHeight()) {
2253 x = getWidth()/2;
2254 y = getHeight()/2;
2257 // Current mouse point in world coords
2258 final double xx = x/magnification + srcRect.x;
2259 final double yy = y/magnification + srcRect.y;
2260 // Delta of view, in screen pixels:
2261 final int px_inc;
2262 if ( 0 != (modifiers & MouseWheelEvent.SHIFT_MASK)) {
2263 if (0 != (modifiers & MouseWheelEvent.ALT_MASK)) px_inc = 1;
2264 else px_inc = 5;
2265 } else px_inc = 20;
2266 final double inc = px_inc/magnification;
2268 final Rectangle r = new Rectangle();
2270 if (rotation > 0) {
2271 // zoom out
2272 r.width = srcRect.width + (int)(inc+0.5);
2273 r.height = srcRect.height + (int)(inc+0.5);
2274 r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
2275 r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
2276 // check boundaries
2277 if (r.width * magnification < getWidth()
2278 || r.height * magnification < getHeight()) {
2279 // Can't zoom at point: would chage field of view's flow or would have to shift the canvas position!
2280 Utils.showStatus("To zoom more, use -/+ keys");
2281 return;
2283 } else {
2284 //zoom in
2285 r.width = srcRect.width - (int)(inc+0.5);
2286 r.height = srcRect.height - (int)(inc+0.5);
2287 if (r.width < 1 || r.height < 1) {
2288 return;
2290 r.x = (int)(xx - ((xx - srcRect.x)/srcRect.width) * r.width + 0.5);
2291 r.y = (int)(yy - ((yy - srcRect.y)/srcRect.height) * r.height + 0.5);
2293 final double newMag = magnification * (srcRect.width / (double)r.width);
2294 // correct floating-point-induced erroneous drift: the int-precision offscreen point under the mouse shoud remain the same
2295 r.x -= (int)((x/newMag + r.x) - xx);
2296 r.y -= (int)((y/newMag + r.y) - yy);
2298 // adjust bounds
2299 int w = (int) Math.round(dstWidth / newMag);
2300 if (w * newMag < dstWidth) w++;
2301 if (w > imageWidth) w = imageWidth;
2302 int h = (int) Math.round(dstHeight / newMag);
2303 if (h * newMag < dstHeight) h++;
2304 if (h > imageHeight) h = imageHeight;
2305 if (r.x < 0) r.x = 0;
2306 if (r.y < 0) r.y = 0;
2307 if (r.x + w > imageWidth) r.x = imageWidth - w;
2308 if (r.y + h > imageHeight) r.y = imageHeight - h; //imageWidth and imageHeight are the LayerSet's width,height, ie. the world's 2D dimensions.
2310 // set!
2311 this.setMagnification(newMag);
2312 this.setSrcRect(r.x, r.y, w, h);
2313 display.repaintAll2();
2315 } else if (0 == (modifiers ^ InputEvent.SHIFT_MASK) && null != display.getActive() && ProjectToolbar.PEN != tool && AreaContainer.class.isInstance(display.getActive())) {
2316 final int sign = rotation > 0 ? 1 : -1;
2317 if (ProjectToolbar.BRUSH == tool) {
2318 int brushSize_old = ProjectToolbar.getBrushSize();
2319 // resize brush for AreaList/AreaTree painting
2320 int brushSize = ProjectToolbar.setBrushSize((int)(5 * sign / magnification)); // the getWheelRotation provides the sign
2321 if (brushSize_old > brushSize) brushSize = brushSize_old; // for repainting purposes alone
2322 int extra = (int)(10 / magnification);
2323 if (extra < 2) extra = 2;
2324 extra += 4; // for good measure
2325 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);
2326 } else if (ProjectToolbar.PENCIL == tool || ProjectToolbar.WAND == tool) {
2327 // resize area to consider for fast-marching
2328 int w = Segmentation.fmp.width;
2329 int h = Segmentation.fmp.height;
2330 Segmentation.fmp.resizeArea(sign, magnification);
2331 w = Math.max(w, Segmentation.fmp.width);
2332 h = Math.max(h, Segmentation.fmp.height);
2333 this.repaint(new Rectangle((int)(mwe.getX() / magnification) + srcRect.x - w/2 + 2,
2334 (int)(mwe.getY() / magnification) + srcRect.y - h/2 + 2,
2335 w + 4, h + 4), 0);
2337 } else if (0 == modifiers) {
2338 // scroll layers
2339 if (rotation > 0) display.nextLayer(modifiers);
2340 else display.previousLayer(modifiers);
2341 } else if (null != display.getActive()) {
2342 // forward to active
2343 display.getActive().mouseWheelMoved(mwe);
2347 protected class RepaintProperties implements AbstractOffscreenThread.RepaintProperties {
2348 final private Layer layer;
2349 final private List<Layer> layers;
2350 final private int g_width;
2351 final private int g_height;
2352 final private Rectangle srcRect;
2353 final private double magnification;
2354 final private Displayable active;
2355 final private int c_alphas;
2356 final private Rectangle clipRect;
2357 final private int mode;
2358 final private HashMap<Color,Layer> hm;
2359 final private ArrayList<LayerPanel> blending_list;
2360 final private GraphicsSource graphics_source;
2362 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) {
2363 this.clipRect = clipRect;
2364 this.layer = layer;
2365 this.layers = layers;
2366 this.g_width = g_width;
2367 this.g_height = g_height;
2368 this.srcRect = srcRect;
2369 this.magnification = magnification;
2370 this.active = active;
2371 this.c_alphas = c_alphas;
2373 // query the display for repainting mode
2374 this.hm = new HashMap<Color,Layer>();
2375 this.blending_list = new ArrayList<LayerPanel>();
2376 this.mode = display.getPaintMode(hm, blending_list);
2377 this.graphics_source = graphics_source;
2381 private final class OffscreenThread extends AbstractOffscreenThread {
2383 OffscreenThread() {
2384 super("T2-Canvas-Offscreen");
2387 public void paint() {
2388 final Layer active_layer;
2389 final List<Layer> layers;
2390 final int g_width;
2391 final int g_height;
2392 final Rectangle srcRect;
2393 final double magnification;
2394 final Displayable active;
2395 final int c_alphas;
2396 final Rectangle clipRect;
2397 final Loader loader;
2398 final HashMap<Color,Layer> hm;
2399 final ArrayList<LayerPanel> blending_list;
2400 final int mode;
2401 final GraphicsSource graphics_source;
2403 synchronized (this) {
2404 final DisplayCanvas.RepaintProperties rp = (DisplayCanvas.RepaintProperties) this.rp;
2405 active_layer = rp.layer;
2406 layers = rp.layers;
2407 g_width = rp.g_width;
2408 g_height = rp.g_height;
2409 srcRect = rp.srcRect;
2410 magnification = rp.magnification;
2411 active = rp.active;
2412 c_alphas = rp.c_alphas;
2413 clipRect = rp.clipRect;
2414 loader = active_layer.getProject().getLoader();
2415 mode = rp.mode;
2416 hm = rp.hm;
2417 blending_list = rp.blending_list;
2418 graphics_source = rp.graphics_source;
2421 BufferedImage target = null;
2423 final ArrayList<Displayable> al_top = new ArrayList<Displayable>();
2425 // Check if the image is cached
2426 Screenshot sc = null;
2427 try {
2428 if (display.getMode().getClass() == DefaultMode.class) {
2429 sc = active_layer.getParent().getScreenshot(new ScreenshotProperties(active_layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source));
2430 if (null != sc) {
2431 //Utils.log2("Using cached screenshot " + sc + " with srcRect " + sc.srcRect);
2432 target = (BufferedImage) loader.getCachedAWT(sc.sid, 0);
2433 if (null == target) active_layer.getParent().removeFromOffscreens(sc); // the image was thrown out of the cache
2434 else if ( (sc.al_top.size() > 0 && sc.al_top.get(0) != display.getActive())
2435 || (0 == sc.al_top.size() && null != display.getActive()) ) {
2436 // Can't accept: different active object
2437 Utils.log2("rejecting: different active object");
2438 target = null;
2439 } else {
2440 al_top.addAll(sc.al_top);
2441 display.applyFilters(target);
2445 } catch (Throwable t) {
2446 IJError.print(t);
2449 //Utils.log2("Found target " + target + "\n with al_top.size() = " + al_top.size());
2451 if (null == target) {
2452 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);
2453 // Store it:
2454 /* CAN'T, may have prePaint in it
2455 if (null != sc && display.getProject().getProperty("look_ahead_cache", 0) > 0) {
2456 sc.assoc(target);
2457 layer.getParent().storeScreenshot(sc);
2462 synchronized (offscreen_lock) {
2463 // only on success:
2464 if (null != offscreen) to_flush.add(offscreen);
2465 offscreen = target;
2466 update_graphics = false;
2467 DisplayCanvas.this.al_top = al_top;
2469 // Outside, otherwise could deadlock
2470 invalidateVolatile();
2472 // Send repaint event, without offscreen graphics
2473 RT.paint(clipRect, false);
2477 /** Looks into the layer and its LayerSet and finds out what needs to be painted, putting it into the three lists.
2478 * @return the index of the first non-image object. */
2479 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) {
2480 layer.getParent().checkBuckets();
2481 layer.checkBuckets();
2482 final Iterator<Displayable> ital = layer.find(srcRect, true).iterator();
2483 final Collection<Displayable> zdal;
2484 final LayerSet layer_set = layer.getParent();
2485 // Which layers to color cue, if any?
2486 if (layer_set.color_cues) {
2487 final Collection<Displayable> atlayer = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
2488 final Set<Displayable> others = new HashSet<Displayable>();
2489 for (final Layer la : layers) {
2490 if (la == layer) continue;
2491 others.addAll(layer_set.roughlyFindZDisplayables(la, srcRect, true));
2493 others.removeAll(atlayer);
2494 zdal = new ArrayList<Displayable>(others); // in whatever order, to paint under
2495 zdal.addAll(atlayer); // in proper stack-index order
2496 } else {
2497 zdal = layer_set.roughlyFindZDisplayables(layer, srcRect, true);
2499 final Iterator<Displayable> itzd = zdal.iterator();
2501 // Assumes the Layer has its objects in order:
2502 // 1 - Patches
2503 // 2 - Profiles, Balls
2504 // 3 - Pipes and ZDisplayables (from the parent LayerSet)
2505 // 4 - DLabels
2507 Displayable tmp = null;
2508 boolean top = false;
2509 final ArrayList<Patch> al_patches = preload_patches ? new ArrayList<Patch>() : null;
2511 int first_non_patch = 0;
2513 while (ital.hasNext()) {
2514 final Displayable d = ital.next();
2515 final Class<?> c = d.getClass();
2516 if (DLabel.class == c || LayerSet.class == c) {
2517 tmp = d; // since ital.next() has moved forward already
2518 break;
2520 if (Patch.class == c) {
2521 al_paint.add(d);
2522 if (preload_patches) al_patches.add((Patch)d);
2523 } else {
2524 if (!top && d == active) top = true; // no Patch on al_top ever
2525 if (top) al_top.add(d); // so active is added to al_top, if it's not a Patch
2526 else al_paint.add(d);
2528 first_non_patch += 1;
2531 // preload concurrently as many as possible
2532 if (preload_patches) Loader.preload(al_patches, magnification, false); // must be false; a 'true' would incur in an infinite loop.
2534 // paint the ZDisplayables here, before the labels and LayerSets, if any
2535 while (itzd.hasNext()) {
2536 final Displayable zd = itzd.next();
2537 if (zd == active) top = true;
2538 if (top) al_top.add(zd);
2539 else al_paint.add(zd);
2541 // paint LayerSet and DLabel objects!
2542 if (null != tmp) {
2543 if (tmp == active) top = true;
2544 if (top) al_top.add(tmp);
2545 else al_paint.add(tmp);
2547 while (ital.hasNext()) {
2548 final Displayable d = ital.next();
2549 if (d == active) top = true;
2550 if (top) al_top.add(d);
2551 else al_paint.add(d);
2554 return first_non_patch;
2557 @Deprecated
2558 public BufferedImage paintOffscreen(final Layer active_layer, final int g_width, final int g_height,
2559 final Rectangle srcRect, final double magnification, final Displayable active,
2560 final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
2561 final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
2562 final boolean prepaint, final ArrayList<Displayable> al_top) {
2563 return paintOffscreen(active_layer, active_layer.getParent().getColorCueLayerRange(active_layer), g_width, g_height, srcRect, magnification, active,
2564 c_alphas, clipRect, loader, hm, blending_list, mode, graphics_source,
2565 prepaint, al_top, false);
2568 /** This method uses data only from the arguments, and changes none.
2569 * Will fill @param al_top with proper Displayable objects, or none when none are selected. */
2570 public BufferedImage paintOffscreen(final Layer active_layer, final List<Layer> layers, final int g_width, final int g_height,
2571 final Rectangle srcRect, final double magnification, final Displayable active,
2572 final int c_alphas, final Rectangle clipRect, final Loader loader, final HashMap<Color,Layer> hm,
2573 final ArrayList<LayerPanel> blending_list, final int mode, final GraphicsSource graphics_source,
2574 final boolean prepaint, final ArrayList<Displayable> al_top, final boolean preload) {
2576 final ArrayList<Displayable> al_paint = new ArrayList<Displayable>();
2577 int first_non_patch = gatherDisplayables(active_layer, layers, srcRect, active, al_paint, al_top, preload);
2579 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);
2582 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) {
2583 try {
2584 if (0 == g_width || 0 == g_height) return null;
2585 // ALMOST, but not always perfect //if (null != clipRect) g.setClip(clipRect);
2587 // prepare the canvas for the srcRect and magnification
2588 final AffineTransform atc = new AffineTransform();
2589 atc.scale(magnification, magnification);
2590 atc.translate(-srcRect.x, -srcRect.y);
2592 // the non-srcRect areas, in offscreen coords
2593 final Rectangle r1 = new Rectangle(srcRect.x + srcRect.width, srcRect.y, (int)(g_width / magnification) - srcRect.width, (int)(g_height / magnification));
2594 final Rectangle r2 = new Rectangle(srcRect.x, srcRect.y + srcRect.height, srcRect.width, (int)(g_height / magnification) - srcRect.height);
2596 // create new graphics
2597 try {
2598 display.getProject().getLoader().releaseToFit(g_width * g_height * 10);
2599 } catch (Exception e) {} // when closing, asynch state may throw for a null loader.
2601 final BufferedImage target = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT); // creates a BufferedImage.TYPE_INT_ARGB image in my T60p ATI FireGL laptop
2602 //Utils.log2("offscreen acceleration priority: " + target.getAccelerationPriority());
2603 final Graphics2D g = target.createGraphics();
2605 g.setTransform(atc); //at_original);
2607 //setRenderingHints(g);
2608 // always a stroke of 1.0, regardless of magnification; the stroke below corrects for that
2609 g.setStroke(stroke);
2613 // Testing: removed Area.subtract, now need to fill in background
2614 g.setColor(Color.black);
2615 g.fillRect(0, 0, g_width - r1.x, g_height - r2.y);
2618 // paint:
2619 // 1 - background
2620 // 2 - images and anything else not on al_top
2621 // 3 - non-srcRect areas
2623 //Utils.log2("offscreen painting: " + al_paint.size());
2625 // filter paintables
2626 final Collection<? extends Paintable> paintables = graphics_source.asPaintable(al_paint);
2628 // adjust:
2629 first_non_patch = paintables.size() - (al_paint.size() - first_non_patch);
2631 // Determine painting mode
2632 if (Display.REPAINT_SINGLE_LAYER == mode) {
2633 if (display.isLiveFilteringEnabled()) {
2634 paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, true);
2635 } else {
2636 // Direct painting mode, with prePaint abilities
2637 int i = 0;
2638 for (final Paintable d : paintables) {
2639 if (i == first_non_patch) {
2640 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2641 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2642 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2643 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2644 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2645 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2647 if (prepaint) d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2648 else d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2649 i++;
2652 } else if (Display.REPAINT_MULTI_LAYER == mode) {
2653 // paint first the current layer Patches only (to set the background)
2654 // With prePaint capabilities:
2655 if (display.isLiveFilteringEnabled()) {
2656 paintWithFiltering(g, al_paint, paintables, first_non_patch, g_width, g_height, active, c_alphas, active_layer, layers, false);
2657 } else {
2658 int i = 0;
2659 if (prepaint) {
2660 for (final Paintable d : paintables) {
2661 if (first_non_patch == i) break;
2662 d.prePaint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2663 i++;
2665 } else {
2666 for (final Paintable d : paintables) {
2667 if (first_non_patch == i) break;
2668 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2669 i++;
2674 // then blend on top the ImageData of the others, in reverse Z order and using the alpha of the LayerPanel
2675 final Composite original = g.getComposite();
2676 // reset
2677 g.setTransform(new AffineTransform());
2678 // Paint what:
2679 final Set<Class<?>> included = display.classes_to_multipaint;
2680 for (final ListIterator<LayerPanel> it = blending_list.listIterator(blending_list.size()); it.hasPrevious(); ) {
2681 final LayerPanel lp = it.previous();
2682 if (lp.layer == active_layer) continue;
2683 active_layer.getProject().getLoader().releaseToFit(g_width * g_height * 4 + 1024);
2684 final BufferedImage bi = getGraphicsConfiguration().createCompatibleImage(g_width, g_height, Transparency.TRANSLUCENT);
2685 final Graphics2D gb = bi.createGraphics();
2686 gb.setTransform(atc);
2687 for (final Displayable d : lp.layer.find(srcRect, true)) {
2688 if (included.contains(d.getClass()))
2689 d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
2691 // Repeating loop ... the human compiler at work, just because one cannot lazily concatenate both sequences:
2692 for (final Displayable d : lp.layer.getParent().roughlyFindZDisplayables(lp.layer, srcRect, true)) {
2693 if (included.contains(d.getClass()))
2694 d.paint(gb, srcRect, magnification, false, c_alphas, lp.layer, layers); // not prePaint! We want direct painting, even if potentially slow
2696 try {
2697 g.setComposite(Displayable.getComposite(display.getLayerCompositeMode(lp.layer), lp.getAlpha()));
2698 g.drawImage(display.applyFilters(bi), 0, 0, null);
2699 } catch (Throwable t) {
2700 Utils.log("Could not use composite mode for layer overlays! Your graphics card may not support it.");
2701 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, lp.getAlpha()));
2702 g.drawImage(bi, 0, 0, null);
2703 IJError.print(t);
2705 bi.flush();
2707 // restore
2708 g.setComposite(original);
2709 g.setTransform(atc);
2711 // then paint the non-Patch objects of the current layer
2713 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2714 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2715 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2716 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2717 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2718 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2720 // TODO this loop should be reading from the paintable_patches and paintables, since their length/order *could* have changed
2721 // For the current layer:
2722 for (int i = first_non_patch; i < al_paint.size(); i++) {
2723 final Displayable d = al_paint.get(i);
2724 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2726 } else if(Display.REPAINT_RGB_LAYER == mode) {
2727 // TODO rewrite to avoid calling the list twice
2728 final Collection<? extends Paintable> paintable_patches = graphics_source.asPaintable(al_paint);
2730 final HashMap<Color,byte[]> channels = new HashMap<Color,byte[]>();
2731 hm.put(Color.green, active_layer);
2732 for (final Map.Entry<Color,Layer> e : hm.entrySet()) {
2733 final BufferedImage bi = new BufferedImage(g_width, g_height, BufferedImage.TYPE_BYTE_GRAY); //INDEXED, Loader.GRAY_LUT);
2734 final Graphics2D gb = bi.createGraphics();
2735 gb.setTransform(atc);
2736 final Layer la = e.getValue();
2737 ArrayList<Paintable> list = new ArrayList<Paintable>();
2738 if (la == active_layer) {
2739 if (Color.green != e.getKey()) continue; // don't paint current layer in two channels
2740 list.addAll(paintable_patches);
2741 } else {
2742 list.addAll(la.find(Patch.class, srcRect, true));
2744 list.addAll(la.getParent().getZDisplayables(ImageData.class, true)); // Stack.class and perhaps others
2745 for (final Paintable d : list) {
2746 d.paint(gb, srcRect, magnification, false, c_alphas, la, layers);
2748 channels.put(e.getKey(), (byte[])new ByteProcessor(bi).getPixels());
2750 final byte[] red, green, blue;
2751 green = channels.get(Color.green);
2752 if (null == channels.get(Color.red)) red = new byte[green.length];
2753 else red = channels.get(Color.red);
2754 if (null == channels.get(Color.blue)) blue = new byte[green.length];
2755 else blue = channels.get(Color.blue);
2756 final int[] pix = new int[green.length];
2757 for (int i=0; i<green.length; i++) {
2758 pix[i] = ((red[i] & 0xff) << 16) + ((green[i] & 0xff) << 8) + (blue[i] & 0xff);
2760 // undo transform, is intended for Displayable objects
2761 g.setTransform(new AffineTransform());
2762 final ColorProcessor cp = new ColorProcessor(g_width, g_height, pix);
2763 if (display.invert_colors) cp.invert();
2764 display.applyFilters(cp);
2765 final Image img = cp.createImage();
2766 g.drawImage(img, 0, 0, null);
2767 img.flush();
2768 // reset
2769 g.setTransform(atc);
2771 // then paint the non-Image objects of the current layer
2772 //Object antialias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
2773 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2774 //Object text_antialias = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
2775 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2776 //Object render_quality = g.getRenderingHint(RenderingHints.KEY_RENDERING);
2777 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2779 for (final Displayable d : al_paint) {
2780 if (ImageData.class.isInstance(d)) continue;
2781 d.paint(g, srcRect, magnification, d == active, c_alphas, active_layer, layers);
2783 // TODO having each object type in a key/list<type> table would be so much easier and likely performant.
2786 // finally, paint non-srcRect areas
2787 if (r1.width > 0 || r1.height > 0 || r2.width > 0 || r2.height > 0) {
2788 g.setColor(Color.gray);
2789 g.setClip(r1);
2790 g.fill(r1);
2791 g.setClip(r2);
2792 g.fill(r2);
2795 return target;
2796 } catch (OutOfMemoryError oome) {
2797 // so OutOfMemoryError won't generate locks
2798 IJError.print(oome);
2799 } catch (Exception e) {
2800 IJError.print(e);
2802 return null;
2805 private final void paintWithFiltering(final Graphics2D g, final ArrayList<Displayable> al_paint,
2806 final Collection<? extends Paintable> paintables,
2807 final int first_non_patch,
2808 final int g_width, final int g_height,
2809 final Displayable active, final int c_alphas,
2810 final Layer layer, final List<Layer> layers, final boolean paint_non_images) {
2811 // Determine the type of the image: if any Patch is of type COLOR_RGB or COLOR_256, use RGB
2812 int type = BufferedImage.TYPE_BYTE_GRAY;
2813 search: for (final Displayable d : al_paint) {
2814 if (d.getClass() == Patch.class) {
2815 switch (((Patch)d).getType()) {
2816 case ImagePlus.COLOR_256:
2817 case ImagePlus.COLOR_RGB:
2818 type = BufferedImage.TYPE_INT_ARGB;
2819 break search;
2824 // Paint all patches to an image
2825 final BufferedImage bi = new BufferedImage(g_width, g_height, type);
2826 final Graphics2D gpre = bi.createGraphics();
2827 gpre.setTransform(atc);
2828 int i = 0;
2829 for (final Paintable p : paintables) {
2830 if (i == first_non_patch) break;
2831 p.paint(gpre, srcRect, magnification, p == active, c_alphas, layer, layers);
2832 i++;
2834 gpre.dispose();
2835 final ImagePlus imp = new ImagePlus("filtered", type == BufferedImage.TYPE_BYTE_GRAY ? new ByteProcessor(bi) : new ColorProcessor(bi));
2836 bi.flush();
2838 display.applyFilters(imp);
2840 // Paint the filtered image
2841 final AffineTransform aff = g.getTransform();
2842 g.setTransform(new AffineTransform()); // reset
2843 g.drawImage(imp.getProcessor().createImage(), 0, 0, null);
2844 // Paint the remaining elements if any
2845 if (paint_non_images && first_non_patch != paintables.size()) {
2846 g.setTransform(aff); // restore srcRect and magnification
2847 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2848 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2849 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2850 i = 0;
2851 for (final Paintable p : paintables) {
2852 if (i < first_non_patch) {
2853 i++;
2854 continue;
2856 p.paint(g, srcRect, magnification, p == active, c_alphas, layer, layers);
2857 i++;
2863 // added here to prevent flickering, but doesn't help. All it does is avoid a call to imp.redraw()
2864 protected void scroll(int sx, int sy) {
2865 int ox = xSrcStart + (int)(sx/magnification); //convert to offscreen coordinates
2866 int oy = ySrcStart + (int)(sy/magnification);
2867 int newx = xSrcStart + (xMouseStart-ox);
2868 int newy = ySrcStart + (yMouseStart-oy);
2869 if (newx<0) newx = 0;
2870 if (newy<0) newy = 0;
2871 if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width;
2872 if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height;
2873 srcRect.x = newx;
2874 srcRect.y = newy;
2875 display.getMode().srcRectUpdated(srcRect, magnification);
2878 private void handleHide(final KeyEvent ke) {
2879 if (ke.isAltDown() && !ke.isShiftDown()) {
2880 // show hidden
2881 Display.updateCheckboxes(display.getLayer().getParent().setAllVisible(false), DisplayablePanel.VISIBILITY_STATE);
2882 //Display.repaint(display.getLayer());
2883 Display.update(display.getLayer());
2884 ke.consume();
2885 return;
2887 if (ke.isShiftDown()) {
2888 // hide deselected
2889 display.hideDeselected(ke.isAltDown());
2890 ke.consume();
2891 return;
2893 // else, hide selected
2894 display.getSelection().setVisible(false);
2895 Display.update(display.getLayer());
2896 ke.consume();
2899 DisplayCanvas.Screenshot createScreenshot(Layer layer) {
2900 return new Screenshot(layer);
2903 protected class ScreenshotProperties {
2904 final Layer layer;
2905 final Rectangle srcRect;
2906 final double magnification;
2907 final int g_width, g_height, c_alphas;
2908 final GraphicsSource graphics_source;
2909 final ArrayList<LayerPanel> blending_list;
2910 final HashMap<Color,Layer> hm;
2911 final int mode;
2912 ScreenshotProperties(Layer layer, Rectangle srcRect, double magnification, int g_width, int g_height, int c_alphas, GraphicsSource graphics_source) {
2913 this.srcRect = new Rectangle(srcRect);
2914 this.magnification = magnification;
2915 this.layer = layer;
2916 this.blending_list = new ArrayList<LayerPanel>();
2917 this.hm = new HashMap<Color,Layer>();
2918 this.mode = display.getPaintMode(hm, blending_list);
2919 this.g_width = g_width;
2920 this.g_height = g_height;
2921 this.graphics_source = graphics_source;
2922 this.c_alphas = c_alphas;
2923 Layer current_layer = display.getLayer();
2924 if (Display.REPAINT_RGB_LAYER == mode) {
2925 Layer red = hm.get(Color.red);
2926 Layer blue = hm.get(Color.blue);
2927 if (null != red || null != blue) {
2928 LayerSet ls = layer.getParent();
2929 int i_layer = ls.indexOf(layer);
2930 int i_current = ls.indexOf(current_layer);
2931 if (null != red) {
2932 int i_red = ls.indexOf(red);
2933 Layer l = red.getParent().getLayer(i_red + i_current - i_layer);
2934 if (null != l) {
2935 hm.put(Color.red, l);
2936 } else {
2937 hm.remove(Color.red);
2940 if (null != blue) {
2941 int i_blue = ls.indexOf(blue);
2942 Layer l = blue.getParent().getLayer(i_blue + i_current - i_layer);
2943 if (null != l) {
2944 hm.put(Color.blue, l);
2945 } else {
2946 hm.remove(Color.blue);
2952 public final boolean equals(final Object o) {
2953 final ScreenshotProperties s = (ScreenshotProperties)o;
2954 return s.layer == this.layer
2955 && s.magnification == this.magnification
2956 && s.srcRect.x == this.srcRect.x && s.srcRect.y == this.srcRect.y
2957 && s.srcRect.width == this.srcRect.width && s.srcRect.height == this.srcRect.height
2958 && s.mode == this.mode
2959 && s.c_alphas == this.c_alphas
2960 && Utils.equalContent(s.blending_list, this.blending_list)
2961 && Utils.equalContent(s.hm, this.hm);
2963 public int hashCode() { return 0; } //$%^&$#@!
2966 public class Screenshot {
2967 final Layer layer;
2968 long sid = Long.MIN_VALUE;
2969 long born = 0;
2970 final ArrayList<Displayable> al_top = new ArrayList<Displayable>();
2971 final ScreenshotProperties props;
2973 Screenshot(Layer layer) {
2974 this(layer, DisplayCanvas.this.srcRect, DisplayCanvas.this.magnification, DisplayCanvas.this.getWidth(), DisplayCanvas.this.getHeight(), DisplayCanvas.this.display.getDisplayChannelAlphas(), DisplayCanvas.this.display.getMode().getGraphicsSource());
2977 Screenshot(Layer layer, Rectangle srcRect, double magnification, int g_width, int g_height, int c_alphas, GraphicsSource graphics_source) {
2978 this.layer = layer;
2979 this.props = new ScreenshotProperties(layer, srcRect, magnification, g_width, g_height, c_alphas, graphics_source);
2982 public long init() {
2983 this.born = System.currentTimeMillis();
2984 this.sid = layer.getProject().getLoader().getNextTempId();
2985 return this.sid;
2987 /** Associate @param img to this, with a new sid. */
2988 public long assoc(BufferedImage img) {
2989 init();
2990 if (null != img) layer.getProject().getLoader().cacheAWT(this.sid, img);
2991 return this.sid;
2993 public void createImage() {
2994 BufferedImage img = paintOffscreen(layer, layer.getParent().getColorCueLayerRange(layer), props.g_width, props.g_height, props.srcRect, props.magnification,
2995 display.getActive(), props.c_alphas, null, layer.getProject().getLoader(),
2996 props.hm, props.blending_list, props.mode, props.graphics_source, false, al_top, false);
2997 layer.getProject().getLoader().cacheAWT(sid, img);
2999 public void flush() {
3000 layer.getProject().getLoader().decacheAWT(sid);
3004 private boolean browseToNodeLayer(final boolean is_shift_down) {
3005 // find visible instances of Tree that are currently painting in the canvas
3006 try {
3007 final Layer active_layer = display.getLayer();
3008 final Point po = getCursorLoc(); // in offscreen coords
3009 for (final ZDisplayable zd : display.getLayerSet().getDisplayableList()) {
3010 if (!zd.isVisible()) continue;
3011 if (!(zd instanceof Tree<?>)) continue;
3012 final Tree<?> t = (Tree<?>)zd;
3013 final Layer la = t.toClosestPaintedNode(active_layer, po.x, po.y, magnification);
3014 if (null == la) continue;
3015 // Else:
3016 display.toLayer(la);
3017 if (!is_shift_down) display.getSelection().clear();
3018 display.getSelection().add(t);
3019 switch (ProjectToolbar.getToolId()) {
3020 case ProjectToolbar.PEN:
3021 case ProjectToolbar.BRUSH:
3022 break;
3023 default:
3024 ProjectToolbar.setTool(ProjectToolbar.PEN);
3025 break;
3027 return true;
3029 } catch (Exception e) {
3030 Utils.log2("Oops: " + e);
3032 return false;
3035 /** Smoothly move the canvas from x0,y0,layer0 to x1,y1,layer1 */
3036 protected void animateBrowsing(final int dx, final int dy) {
3037 // check preconditions
3038 final float mag = (float)this.magnification;
3039 final Rectangle startSrcRect = (Rectangle)this.srcRect.clone();
3040 // The motion will be displaced by some screen pixels at every time step.
3041 final Vector2f v = new Vector2f(dx, dy);
3042 final float sqdist_to_travel = v.lengthSquared();
3043 v.normalize();
3044 v.scale(20/mag);
3045 final Point2f cp = new Point2f(0, 0); // the current deltas
3047 final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
3048 sf[0] = animate(new Runnable() {
3049 public void run() {
3050 cp.add(v);
3051 //Utils.log2("advanced by x,y = " + cp.x + ", " + cp.y);
3052 int x, y;
3053 if (v.lengthSquared() >= sqdist_to_travel) {
3054 // set target position
3055 x = startSrcRect.x + dx;
3056 y = startSrcRect.y + dy;
3057 // quit animation
3058 cancelAnimation(sf[0]);
3059 } else {
3060 // set position
3061 x = startSrcRect.x + (int)(cp.x);
3062 y = startSrcRect.y + (int)(cp.y);
3064 setSrcRect(x, y, startSrcRect.width, startSrcRect.height);
3065 display.repaintAll2();
3067 }, 0, 50, TimeUnit.MILLISECONDS);
3070 /** Smoothly move the canvas from its current position until the given rectangle is included within the srcRect.
3071 * If the given rectangle is larger than the srcRect, it will refuse to work (for now). */
3072 public boolean animateBrowsing(final Rectangle target_, final Layer target_layer) {
3073 // Crop target to world's 2D dimensions
3074 Area a = new Area(target_);
3075 a.intersect(new Area(display.getLayerSet().get2DBounds()));
3076 final Rectangle target = a.getBounds();
3077 if (0 == target.width || 0 == target.height) {
3078 return false;
3080 // animate at all?
3081 if (this.srcRect.contains(target) && target_layer == display.getLayer()) {
3082 // So: don't animate, but at least highlight the target
3083 playHighlight(target);
3084 return false;
3087 // The motion will be displaced by some screen pixels at every time step.
3088 final int ox = srcRect.x + srcRect.width/2;
3089 final int oy = srcRect.y + srcRect.height/2;
3090 final int tx = target.x + target.width/2;
3091 final int ty = target.y + target.height/2;
3092 final Vector2f v = new Vector2f(tx - ox, ty - oy);
3093 v.normalize();
3094 v.scale(20/(float)magnification);
3097 // The layer range
3098 final Layer start_layer = display.getLayer();
3100 int ithis = display.getLayerSet().indexOf(start_layer);
3101 int itarget = display.getLayerSet().indexOf(target_layer);
3102 final java.util.List<Layer> layers = display.getLayerSet().getLayers(ithis, itarget);
3104 Calibration cal = display.getLayerSet().getCalibrationCopy();
3105 final double pixelWidth = cal.pixelWidth;
3106 final double pixelHeight = cal.pixelHeight;
3108 //final double dist_to_travel = Math.sqrt(Math.pow((tx - ox)*pixelWidth, 2) + Math.pow((ty - oy)*pixelHeight, 2)
3109 // + Math.pow((start_layer.getZ() - target_layer.getZ()) * pixelWidth, 2));
3111 // vector in calibrated coords between origin and target
3112 final Vector3d g = new Vector3d((tx - ox)*pixelWidth, (ty - oy)*pixelHeight, (target_layer.getZ() - start_layer.getZ())*pixelWidth);
3114 final ScheduledFuture<?>[] sf = new ScheduledFuture[1];
3115 sf[0] = animate(new Runnable() {
3116 public void run() {
3117 if (DisplayCanvas.this.srcRect.contains(target)) {
3118 // reached destination
3119 if (display.getLayer() != target_layer) display.toLayer(target_layer);
3120 playHighlight(target);
3121 cancelAnimation(sf[0]);
3122 } else {
3123 setSrcRect(srcRect.x + (int)v.x, srcRect.y + (int)v.y, srcRect.width, srcRect.height);
3124 // which layer?
3125 if (start_layer != target_layer) {
3126 int cx = srcRect.x + srcRect.width/2;
3127 int cy = srcRect.y + srcRect.height/2;
3128 double dist = Math.sqrt(Math.pow((cx - ox)*pixelWidth, 2) + Math.pow((cy - oy)*pixelHeight, 2)
3129 + Math.pow((display.getLayer().getZ() - start_layer.getZ()) * pixelWidth, 2));
3131 Vector3d gg = new Vector3d(g);
3132 gg.normalize();
3133 gg.scale((float)dist);
3134 Layer la = display.getLayerSet().getNearestLayer(start_layer.getZ() + gg.z/pixelWidth);
3135 if (la != display.getLayer()) {
3136 display.toLayer(la);
3139 display.repaintAll2();
3142 }, 0, 50, TimeUnit.MILLISECONDS);
3143 return true;
3146 private ScheduledExecutorService animator = null;
3147 private boolean zoom_and_pan = true;
3148 private final Vector<ScheduledFuture<?>> sfs = new Vector<ScheduledFuture<?>>();
3150 private void cancelAnimations() {
3151 if (sfs.isEmpty()) return;
3152 Vector<ScheduledFuture<?>> sfs;
3153 synchronized (this.sfs) { sfs = new Vector<ScheduledFuture<?>>(this.sfs); }
3154 for (ScheduledFuture<?> sf : sfs) {
3155 sf.cancel(true);
3157 this.sfs.clear();
3158 try {
3159 // wait
3160 Thread.sleep(150);
3161 } catch (InterruptedException ie) {}
3162 // Re-enable input, in case the watcher task is canceled as well:
3163 // (It's necessary since there isn't any easy way to tell the scheduler to execute a code block when it cancels its tasks).
3164 restoreUserInput();
3166 private void cancelAnimation(final ScheduledFuture<?> sf) {
3167 sfs.remove(sf);
3168 sf.cancel(true);
3169 restoreUserInput();
3172 private void restoreUserInput() {
3173 zoom_and_pan = true;
3174 display.getProject().setReceivesInput(true);
3177 private ScheduledFuture<?> animate(Runnable run, long initialDelay, long delay, TimeUnit units) {
3178 initAnimator();
3179 // Cancel any animations currently running
3180 cancelAnimations();
3181 // Disable user input
3182 display.getProject().setReceivesInput(false);
3183 zoom_and_pan = false;
3184 // Create tasks to run periodically: a task and a watcher task
3185 final ScheduledFuture<?>[] sf = new ScheduledFuture[2];
3186 sf[0] = animator.scheduleWithFixedDelay(run, initialDelay, delay, units);
3187 sf[1] = animator.scheduleWithFixedDelay(new Runnable() {
3188 public void run() {
3189 if (sf[0].isCancelled()) {
3190 // Enable user input
3191 zoom_and_pan = true;
3192 display.getProject().setReceivesInput(true);
3193 // cancel yourself
3194 sf[1].cancel(true);
3197 }, 100, 700, TimeUnit.MILLISECONDS);
3198 // Store task for future cancelation
3199 sfs.add(sf[0]);
3200 // but not the watcher task, which must finish on its own after the main task finishes.
3201 return sf[0];
3204 /** Draw a dotted circle centered on the given Rectangle. */
3205 private final class Highlighter {
3206 Ellipse2D.Float elf;
3207 final Stroke stroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3, new float[]{4,4,4,4}, 0);
3208 final float dec;
3209 final Rectangle target;
3210 Highlighter(final Rectangle target) {
3211 this.target = target;
3212 elf = new Ellipse2D.Float(target.x, target.y, target.width, target.height);
3213 display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
3214 dec = (float)((Math.max(target.width, target.height)*magnification / 10)/magnification);
3216 boolean next() {
3217 invalidateVolatile();
3218 repaint(target, 5, false);
3219 // setup next iteration
3220 display.getLayerSet().getOverlay().remove(elf);
3221 Ellipse2D.Float elf2 = (Ellipse2D.Float) elf.clone();
3222 elf2.x += dec;
3223 elf2.y += dec;
3224 elf2.width -= (dec+dec);
3225 elf2.height -= (dec+dec);
3226 if (elf2.width > 1 || elf2.height > 1) {
3227 elf = elf2;
3228 display.getLayerSet().getOverlay().add(elf, Color.yellow, stroke, true);
3229 return true;
3230 } else {
3231 display.getLayerSet().getOverlay().remove(elf);
3232 return false;
3235 void cleanup() {
3236 display.getLayerSet().getOverlay().remove(elf);
3240 private interface Animation extends Runnable {}
3242 private ScheduledFuture<?> playHighlight(final Rectangle target) {
3243 initAnimator();
3244 final Highlighter highlight = new Highlighter(target);
3245 final ScheduledFuture<?>[] sf = (ScheduledFuture<?>[])new ScheduledFuture[2];
3246 sf[0] = animator.scheduleWithFixedDelay(new Animation() {
3247 public void run() {
3248 if (!highlight.next()) {
3249 cancelAnimation(sf[0]);
3250 highlight.cleanup();
3253 }, 10, 100, TimeUnit.MILLISECONDS);
3254 sf[1] = animator.scheduleWithFixedDelay(new Animation() {
3255 public void run() {
3256 if (sf[0].isCancelled()) {
3257 highlight.cleanup();
3258 sf[1].cancel(true); // itself
3261 }, 50, 100, TimeUnit.MILLISECONDS);
3262 sfs.add(sf[0]);
3263 return sf[0];
3266 synchronized private void initAnimator() {
3267 if (null == animator) animator = Executors.newScheduledThreadPool(2);