/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Icy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.painter; import icy.canvas.IcyCanvas; import icy.common.CollapsibleEvent; import icy.common.UpdateEventHandler; import icy.common.listener.ChangeListener; import icy.file.xml.XMLPersistent; import icy.gui.viewer.Viewer; import icy.main.Icy; import icy.painter.OverlayEvent.OverlayEventType; import icy.sequence.Sequence; import icy.system.IcyExceptionHandler; import icy.type.point.Point5D; import icy.util.ClassUtil; import icy.util.StringUtil; import icy.util.XMLUtil; import java.awt.Graphics2D; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.Point2D; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; import org.w3c.dom.Node; /** * Overlay class.<br> * <br> * This class allow interaction and rich informations display on Sequences.<br> * {@link IcyCanvas} subclasses should propagate mouse and key events to overlay. * * @author Stephane */ @SuppressWarnings("deprecation") public abstract class Overlay implements Painter, ChangeListener, Comparable<Overlay>, XMLPersistent { /** * Define the overlay priority: * * <pre> * Lowest | BACKGROUND (below image) * | IMAGE (image level) * | SHAPE (just over the image) * | TEXT (over image and shape) * | TOOLTIP (all over the rest) * Highest | TOPMOST (absolute topmost) * </pre> * * You have 4 levels for each category (except TOPMOST) for finest adjustment: * * <pre> * Lowest | LOW * | NORMAL * | HIGH * Highest | TOP * </pre> * * TOP level should be used to give <i>focus<i> to a specific Overlay over all other in the same * category. */ public static enum OverlayPriority { BACKGROUND_LOW, BACKGROUND_NORMAL, BACKGROUND_HIGH, BACKGROUND_TOP, IMAGE_LOW, IMAGE_NORMAL, IMAGE_HIGH, IMAGE_TOP, SHAPE_LOW, SHAPE_NORMAL, SHAPE_HIGH, SHAPE_TOP, TEXT_LOW, TEXT_NORMAL, TEXT_HIGH, TEXT_TOP, TOOLTIP_LOW, TOOLTIP_NORMAL, TOOLTIP_HIGH, TOOLTIP_TOP, TOPMOST } public static final String ID_OVERLAY = "overlay"; public static final String ID_CLASSNAME = "classname"; public static final String ID_ID = "id"; public static final String ID_NAME = "name"; public static final String ID_PRIORITY = "priority"; public static final String ID_READONLY = "readOnly"; public static final String ID_CANBEREMOVED = "canBeRemoved"; public static final String ID_RECEIVEKEYEVENTONHIDDEN = "receiveKeyEventOnHidden"; public static final String ID_RECEIVEMOUSEEVENTONHIDDEN = "receiveMouseEventOnHidden"; public static final String PROPERTY_NAME = ID_NAME; public static final String PROPERTY_PRIORITY = ID_PRIORITY; public static final String PROPERTY_READONLY = ID_READONLY; public static final String PROPERTY_PERSISTENT = "persitent"; public static final String PROPERTY_CANBEREMOVED = ID_CANBEREMOVED; public static final String PROPERTY_RECEIVEKEYEVENTONHIDDEN = ID_RECEIVEKEYEVENTONHIDDEN; public static final String PROPERTY_RECEIVEMOUSEEVENTONHIDDEN = ID_RECEIVEMOUSEEVENTONHIDDEN; /** * We consider as tiny object anything with a size of 10 pixels or less */ public static final int LOD_SMALL = 10; public static final int LOD_TINY = 4; protected static int id_gen = 1; /** * Create a Overlay from a XML node. * * @param node * XML node defining the overlay * @return the created Overlay or <code>null</code> if the Overlay class does not support XML * persistence a default * constructor */ public static Overlay createFromXML(Node node) { if (node == null) return null; final String className = XMLUtil.getElementValue(node, ID_CLASSNAME, ""); if (StringUtil.isEmpty(className)) return null; final Overlay result; try { // search for the specified className final Class<?> clazz = ClassUtil.findClass(className); // class found if (clazz != null) { final Class<? extends Overlay> overlayClazz = clazz.asSubclass(Overlay.class); // default constructor final Constructor<? extends Overlay> constructor = overlayClazz.getConstructor(new Class[] {}); // build Overlay result = constructor.newInstance(); // load properties from XML if (result != null) { // error while loading infos --> return null if (!result.loadFromXML(node)) return null; } return result; } } catch (NoSuchMethodException e) { IcyExceptionHandler.handleException(new NoSuchMethodException("Default constructor not found in class '" + className + "', cannot create the Overlay."), true); } catch (ClassNotFoundException e) { IcyExceptionHandler.handleException(new ClassNotFoundException("Cannot find '" + className + "' class, cannot create the Overlay."), true); } catch (Exception e) { IcyExceptionHandler.handleException(e, true); } return null; } /** * Return the number of Overlay defined in the specified XML node. * * @param node * XML node defining the Overlay list * @return the number of Overlay defined in the XML node. */ public static int getOverlayCount(Node node) { if (node != null) { final List<Node> nodesOverlay = XMLUtil.getChildren(node, ID_OVERLAY); if (nodesOverlay != null) return nodesOverlay.size(); } return 0; } /** * Return a list of Overlay from a XML node. * * @param node * XML node defining the Overlay list * @return a list of Overlay */ public static List<Overlay> loadOverlaysFromXML(Node node) { final List<Overlay> result = new ArrayList<Overlay>(); if (node != null) { final List<Node> nodesOverlay = XMLUtil.getChildren(node, ID_OVERLAY); if (nodesOverlay != null) { for (Node n : nodesOverlay) { final Overlay overlay = createFromXML(n); if (overlay != null) { // we assume this overlay should stay persistent then overlay.setPersistent(true); result.add(overlay); } } } } return result; } /** * Set a list of Overlay to a XML node. * * @param node * XML node which is used to store the list of Overlay * @param overlays * the list of Overlay to store in the XML node */ public static void saveOverlaysToXML(Node node, List<Overlay> overlays) { if (node != null) { for (Overlay overlay : overlays) { // only save persistent overlay if (overlay.isPersistent()) { final Node nodeOverlay = XMLUtil.addElement(node, ID_OVERLAY); if (!overlay.saveToXML(nodeOverlay)) { XMLUtil.removeNode(node, nodeOverlay); System.err.println("Error: the overlay " + overlay.getName() + " was not correctly saved to XML !"); } } } } } /** * properties */ protected int id; protected String name; protected OverlayPriority priority; protected boolean persistent; protected boolean readOnly; protected boolean canBeRemoved; protected boolean receiveKeyEventOnHidden; protected boolean receiveMouseEventOnHidden; /** * internals */ protected final List<OverlayListener> listeners; protected final UpdateEventHandler updater; public Overlay(String name, OverlayPriority priority) { super(); synchronized (Overlay.class) { id = id_gen++; } this.name = name; this.priority = priority; // by default the overlay is not persistent persistent = false; readOnly = false; canBeRemoved = true; receiveKeyEventOnHidden = false; receiveMouseEventOnHidden = false; listeners = new ArrayList<OverlayListener>(); updater = new UpdateEventHandler(this, false); } public Overlay(String name) { // create overlay with default priority this(name, OverlayPriority.SHAPE_NORMAL); } /** * @return the name */ public String getName() { return name; } /** * @param name * the name to set */ public void setName(String name) { if (this.name != name) { this.name = name; propertyChanged(PROPERTY_NAME); } } /** * @return the priority */ public OverlayPriority getPriority() { return priority; } /** * @param priority * the priority to set */ public void setPriority(OverlayPriority priority) { if (this.priority != priority) { this.priority = priority; propertyChanged(PROPERTY_PRIORITY); } } /** * Returns <code>true</code> if the overlay is attached to the specified {@link Sequence}. */ public boolean isAttached(Sequence sequence) { if (sequence != null) return sequence.contains(this); return false; } /** * @deprecated Use {@link #getCanBeRemoved()} instead. * @see #setCanBeRemoved(boolean) */ @Deprecated public boolean isFixed() { return !getCanBeRemoved(); } /** * @deprecated Use {@link #setCanBeRemoved(boolean)} instead. */ @Deprecated public void setFixed(boolean value) { setCanBeRemoved(!value); } /** * Returns <code>true</code> if the overlay can be freely removed from the Canvas where it * appears and <code>false</code> otherwise.<br/> * * @see #setCanBeRemoved(boolean) */ public boolean getCanBeRemoved() { return canBeRemoved; } /** * Set the <code>canBeRemoved</code> property.<br/> * Set it to false if you want to prevent the overlay to be removed from the Canvas where it * appears. */ public void setCanBeRemoved(boolean value) { if (canBeRemoved != value) { canBeRemoved = value; propertyChanged(PROPERTY_CANBEREMOVED); } } /** * Return persistent property.<br/> * When set to <code>true</code> the Overlay will be saved in the Sequence persistent XML data. */ public boolean isPersistent() { return persistent; } /** * Set persistent property.<br/> * When set to <code>true</code> the Overlay will be saved in the Sequence persistent XML data * (default is <code>false</code>). */ public void setPersistent(boolean value) { if (persistent != value) { persistent = value; propertyChanged(PROPERTY_PERSISTENT); } } /** * Return read only property.<br/> * When set to <code>true</code> we cannot anymore modify overlay properties from the GUI. */ public boolean isReadOnly() { return readOnly; } /** * Set read only property.<br/> * When set to <code>true</code> we cannot anymore modify overlay properties from the GUI. */ public void setReadOnly(boolean value) { if (readOnly != value) { readOnly = value; propertyChanged(PROPERTY_READONLY); } } /** * @return <code>true</code> is the overlay should receive {@link KeyEvent} even when it is not * visible. */ public boolean getReceiveKeyEventOnHidden() { return receiveKeyEventOnHidden; } /** * Set to <code>true</code> if you want to overlay to receive {@link KeyEvent} even when it is * not visible. */ public void setReceiveKeyEventOnHidden(boolean value) { if (receiveKeyEventOnHidden != value) { receiveKeyEventOnHidden = value; propertyChanged(PROPERTY_RECEIVEKEYEVENTONHIDDEN); } } /** * @return <code>true</code> is the overlay should receive {@link MouseEvent} even when it is * not visible. */ public boolean getReceiveMouseEventOnHidden() { return receiveMouseEventOnHidden; } /** * Set to <code>true</code> if you want to overlay to receive {@link KeyEvent} even when it is * not visible. */ public void setReceiveMouseEventOnHidden(boolean value) { if (receiveMouseEventOnHidden != value) { receiveMouseEventOnHidden = value; propertyChanged(PROPERTY_RECEIVEMOUSEEVENTONHIDDEN); } } /** * Override this method to provide an extra options panel for the overlay.<br> * The options panel will appears in the inspector when the layer's overlay is selected. */ public JPanel getOptionsPanel() { return null; } /** * @deprecated Use {@link Sequence#addOverlay(Overlay)} instead. */ @Deprecated public void attachTo(Sequence sequence) { if (sequence != null) sequence.addOverlay(this); } /** * @deprecated Use {@link Sequence#removeOverlay(Overlay)} instead. */ @Deprecated public void detachFrom(Sequence sequence) { if (sequence != null) sequence.removeOverlay(this); } /** * Remove the Overlay from all sequences and canvas where it is currently attached. */ public void remove() { for (Sequence sequence : getSequences()) sequence.removeOverlay(this); for (IcyCanvas canvas : getAttachedCanvas()) canvas.removeLayer(this); } /** * Returns all sequences where the overlay is currently attached. */ public List<Sequence> getSequences() { return Icy.getMainInterface().getSequencesContaining(this); } /** * Returns all canvas where the overlay is currently present as a layer. */ public List<IcyCanvas> getAttachedCanvas() { final List<IcyCanvas> result = new ArrayList<IcyCanvas>(); for (Viewer viewer : Icy.getMainInterface().getViewers()) { final IcyCanvas canvas = viewer.getCanvas(); if ((canvas != null) && canvas.hasLayer(this)) result.add(canvas); } return result; } public void beginUpdate() { updater.beginUpdate(); } public void endUpdate() { updater.endUpdate(); } public boolean isUpdating() { return updater.isUpdating(); } /** * @deprecated Use {@link #painterChanged()} instead. */ @Deprecated public void changed() { painterChanged(); } /** * Notify the painter content has changed.<br> * All sequence containing the overlay will be repainted to reflect the change. */ public void painterChanged() { updater.changed(new OverlayEvent(this, OverlayEventType.PAINTER_CHANGED)); } /** * Notify the overlay property has changed. */ public void propertyChanged(String propertyName) { updater.changed(new OverlayEvent(this, OverlayEventType.PROPERTY_CHANGED, propertyName)); } @Override public void onChanged(CollapsibleEvent object) { fireOverlayChangedEvent((OverlayEvent) object); } protected void fireOverlayChangedEvent(OverlayEvent event) { for (OverlayListener listener : new ArrayList<OverlayListener>(listeners)) listener.overlayChanged(event); } /** * Add a listener. */ public void addOverlayListener(OverlayListener listener) { listeners.add(listener); } /** * Remove a listener. */ public void removeOverlayListener(OverlayListener listener) { listeners.remove(listener); } /** * Paint method called to draw the overlay. */ @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { // nothing by default } /** * @deprecated Use {@link #mousePressed(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void mousePressed(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseReleased(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void mouseReleased(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseClick(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void mouseClick(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseMove(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void mouseMove(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseDrag(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void mouseDrag(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseEntered(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated public void mouseEntered(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseExited(MouseEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated public void mouseExited(MouseEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #mouseWheelMoved(MouseWheelEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated public void mouseWheelMoved(MouseWheelEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #keyPressed(KeyEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void keyPressed(KeyEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * @deprecated Use {@link #keyReleased(KeyEvent, Point5D.Double, IcyCanvas)} instead */ @Deprecated @Override public void keyReleased(KeyEvent e, Point2D imagePoint, IcyCanvas canvas) { // no action by default } /** * Mouse press event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mousePressed(e, imagePoint.toPoint2D(), canvas); else mousePressed(e, (Point2D) null, canvas); } /** * Mouse release event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseReleased(e, imagePoint.toPoint2D(), canvas); else mouseReleased(e, (Point2D) null, canvas); } /** * Mouse click event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseClick(e, imagePoint.toPoint2D(), canvas); else mouseClick(e, (Point2D) null, canvas); } /** * Mouse move event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseMove(e, imagePoint.toPoint2D(), canvas); else mouseMove(e, (Point2D) null, canvas); } /** * Mouse drag event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseDrag(e, imagePoint.toPoint2D(), canvas); else mouseDrag(e, (Point2D) null, canvas); } /** * Mouse enter event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseEntered(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseEntered(e, imagePoint.toPoint2D(), canvas); else mouseEntered(e, (Point2D) null, canvas); } /** * Mouse exit event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseExited(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseExited(e, imagePoint.toPoint2D(), canvas); else mouseExited(e, (Point2D) null, canvas); } /** * Mouse wheel moved event forwarded to the overlay. * * @param e * mouse event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void mouseWheelMoved(MouseWheelEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) mouseWheelMoved(e, imagePoint.toPoint2D(), canvas); else mouseWheelMoved(e, (Point2D) null, canvas); } /** * Key press event forwarded to the overlay. * * @param e * key event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) keyPressed(e, imagePoint.toPoint2D(), canvas); else keyPressed(e, (Point2D) null, canvas); } /** * Key release event forwarded to the overlay. * * @param e * key event * @param imagePoint * mouse position (image coordinates) * @param canvas * icy canvas */ public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // provide backward compatibility if (imagePoint != null) keyReleased(e, imagePoint.toPoint2D(), canvas); else keyReleased(e, (Point2D) null, canvas); } public boolean loadFromXML(Node node, boolean preserveId) { if (node == null) return false; beginUpdate(); try { // FIXME : this can make duplicate id but it is also important to preserve id if (!preserveId) { id = XMLUtil.getElementIntValue(node, ID_ID, 0); synchronized (Overlay.class) { // avoid having same id if (id_gen <= id) id_gen = id + 1; } } setName(XMLUtil.getElementValue(node, ID_NAME, "")); setPriority(OverlayPriority.values()[XMLUtil.getElementIntValue(node, ID_PRIORITY, OverlayPriority.SHAPE_NORMAL.ordinal())]); setReadOnly(XMLUtil.getElementBooleanValue(node, ID_READONLY, false)); setReceiveKeyEventOnHidden(XMLUtil.getElementBooleanValue(node, ID_RECEIVEKEYEVENTONHIDDEN, false)); setReceiveMouseEventOnHidden(XMLUtil.getElementBooleanValue(node, ID_RECEIVEMOUSEEVENTONHIDDEN, false)); } finally { endUpdate(); } return true; } @Override public boolean loadFromXML(Node node) { return loadFromXML(node, false); } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setElementValue(node, ID_CLASSNAME, getClass().getName()); XMLUtil.setElementIntValue(node, ID_ID, id); XMLUtil.setElementValue(node, ID_NAME, getName()); XMLUtil.setElementIntValue(node, ID_PRIORITY, getPriority().ordinal()); XMLUtil.setElementBooleanValue(node, ID_READONLY, isReadOnly()); XMLUtil.setElementBooleanValue(node, ID_RECEIVEKEYEVENTONHIDDEN, getReceiveKeyEventOnHidden()); XMLUtil.setElementBooleanValue(node, ID_RECEIVEMOUSEEVENTONHIDDEN, getReceiveMouseEventOnHidden()); return true; } @Override public int compareTo(Overlay o) { // highest priority first return o.priority.ordinal() - priority.ordinal(); } }