/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */

package net.sf.jaxodraw.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.Rectangle2D;

import java.beans.PropertyChangeEvent;

import java.io.IOException;
import java.io.ObjectInputStream;

import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import net.sf.jaxodraw.graph.JaxoGraph;
import net.sf.jaxodraw.gui.grid.JaxoPaintableGrid;
import net.sf.jaxodraw.gui.handle.JaxoDefaultHandle;
import net.sf.jaxodraw.gui.menu.popup.JaxoFBoxPopupMenu;
import net.sf.jaxodraw.gui.panel.JaxoColorChooser;
import net.sf.jaxodraw.gui.panel.edit.JaxoOptionsPanel;
import net.sf.jaxodraw.object.JaxoFillColorObject;
import net.sf.jaxodraw.object.JaxoHandle;
import net.sf.jaxodraw.object.JaxoList;
import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectEditPanel;
import net.sf.jaxodraw.object.group.JaxoGroup;
import net.sf.jaxodraw.object.text.JaxoPSText;
import net.sf.jaxodraw.util.JaxoColor;
import net.sf.jaxodraw.util.JaxoConstants;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.JaxoPrefs;
import net.sf.jaxodraw.util.Location;


/**
 * Abstract base class for the canvas.
 *
 * @since 2.1
 */
public abstract class AbstractJaxoCanvas extends JComponent implements JaxoDrawingArea {

    /**
     * Do not paint handles.
     */
    protected static final int HANDLE_PAINT_OFF = 0;

    /**
     * Paint handles.
     */
    protected static final int HANDLE_PAINT_ON = 1;

    /**
     * Only paint handles of selected objects.
     */
    protected static final int HANDLE_PAINT_SELECTION = 2;

    protected static final boolean HOVERING_EDITED_OBJECTS = true;

    // Padding added to the bounding box to determine the area to be repainted
    private static final Insets COMPONENT_REPAINT_PADDING = new Insets(5, 5, 5, 5);

    private transient JaxoHandle handle = new JaxoDefaultHandle();
    private transient JaxoZoom zoom = new JaxoZoom(this);
    private transient JaxoPaintableGrid grid;
    private transient ChangeListener gridListener = getNewGridListener();
    private transient JaxoGraph canvasGraph = new JaxoGraph(); // should not be null
    private transient JaxoColorChooser colorChooser;
    private final JaxoFBoxPopupMenu fboxPopup;

    private final JaxoClipboard clipboard;

    private boolean antialiasEnabled;
    private Color canvasBackground = Color.white;

    private final Dimension minimumCanvasSize = new Dimension(0, 0);
    private final Dimension maximumCanvasSize = new Dimension(7 * 72, 5 * 72);

    /** indicates how/whether to paint the handles of the objects. */
    private int handlePaintMode;

    /**
     * Constructor.
     */
    protected AbstractJaxoCanvas() {
        super();
        fboxPopup = new JaxoFBoxPopupMenu();
        clipboard = new JaxoClipboard(this);
        resetColorChoser();
    }

    /**
     * The JaxoFBoxPopupMenu for this canvas.
     *
     * @return a JaxoFBoxPopupMenu, not null.
     */
    protected final JaxoFBoxPopupMenu getFboxPopup() {
        return fboxPopup;
    }

    private void readObject(final ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.zoom = new JaxoZoom(this);
        this.handle = new JaxoDefaultHandle();
        this.canvasGraph = new JaxoGraph();
        resetColorChoser();
        this.gridListener = getNewGridListener();
    }

    /** {@inheritDoc} */
    public void propertyChange(final PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals("Jaxo.mode")) {
            final int mode = ((Integer) evt.getNewValue()).intValue();

            if (!unMarkGraph()) {
                repaint(); // handles etc., should not be needed in theory
            }

            zoom.setActive(mode == JaxoConstants.ZOOM);
            updateMode(mode);
            firePropertyChange("Jaxo.mode", evt.getOldValue(), evt.getNewValue());
        } else if (evt.getPropertyName().equals("Jaxo.antialiasEnabled")) {
            setAntialiasEnabled(((Boolean) evt.getNewValue()).booleanValue());
        } else if (evt.getPropertyName().equals("Jaxo.mouseLocation")) {
            firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
        }
    }

    /** {@inheritDoc} */
    public Component asComponent() {
        return this;
    }

    /**
     * The currently set handle.
     *
     * @return The current handle.
     */
    protected JaxoHandle getHandle() {
        return handle;
    }

    /**
     * Sets a new handle.
     *
     * @param newHandle The handle to set.
     */
    public void setHandle(final JaxoHandle newHandle) {
        if (newHandle != getHandle()) {
            handle = newHandle;
            repaint();
        }
    }

    /**
     * How to paint handles.
     *
     * @return the current handle paint mode.
     */
    protected int getHandlePaintMode() {
        return handlePaintMode;
    }

    /**
     * Sets how to paint handles.
     *
     * @param value One of the HANDLE_PAINT constants.
     */
    protected void setHandlePaintMode(final int value) {
        if (handlePaintMode != value) {
            handlePaintMode = value;
            repaint();
        }
    }

    /**
     * Returns the current graph.
     *
     * @return The current graph.
     */
    // NOTE: from outside, the graph should be retrieved from the tab, not the canvas!
    protected JaxoGraph getCanvasGraph() {
        return this.canvasGraph;
    }

    /** {@inheritDoc} */
    public void setCanvasGraph(final JaxoGraph value) {
        this.canvasGraph = value;
    }

    /** {@inheritDoc} */
    public void moveGraph(final int dx, final int dy) {
        canvasGraph.getObjectList().moveAllObjects(dx, dy);
        markImageInvalid();
    }

    /** {@inheritDoc} */
    public void copyMarkedObjects() {
        putToSystemClipboard(getClipboard());
    }

    /** {@inheritDoc} */
    public void cutMarkedObjects() {
        copyMarkedObjects();
        deleteMarkedObjects();
    }

    /** {@inheritDoc} */
    public boolean unMarkGraph() {
        if (canvasGraph.containsMarkedObjects()) {
            canvasGraph.setAsMarked(false);
            repaint(); // for handles

            return true;
        }

        return false;
    }

    /**
     * Background color in the region that is actually covered by the canvas
     * {@link #getCanvasSize}, which is the whole canvas, unless
     * the effective maximum size is smaller than the component size.
     * Note that this color may be overwritten by the grid.
     *
     * @return The canvas background color.
     */
    public Color getCanvasBackground() {
        return canvasBackground;
    }

    /** {@inheritDoc} */
    public void setCanvasBackground(final Color color) {
        if (!canvasBackground.equals(color)) {
            canvasBackground = color;
            markBackgroundInvalid();
        }
    }

    /** {@inheritDoc} */
    public Point getCanvasOrigin() {
        final Insets n = getInsets();

        return new Point(n.left, n.top);
    }

    /** {@inheritDoc} */
    public Rectangle getCanvasBounds() {
        final Insets n = getInsets();
        final Dimension d = getCanvasSize();

        return new Rectangle(n.left, n.top, d.width, d.height);
    }

    /** {@inheritDoc} */
    public Dimension getCanvasSize() {
        final Dimension result = getSize();
        final Insets n = getInsets();
        final Dimension maximum = getEffectiveMaximumCanvasSize();

        result.width = Math.min(maximum.width, result.width - n.left - n.right);
        result.height = Math.min(maximum.height, result.height - n.top - n.bottom);

        return result;
    }

    /** {@inheritDoc} */
    public Dimension getMinimumCanvasSize() {
        return (Dimension) minimumCanvasSize.clone();
    }

    /** {@inheritDoc} */
    public void setMinimumCanvasSize(final Dimension value) {
        if (!value.equals(minimumCanvasSize)) {
            final Dimension old = getEffectiveMaximumCanvasSize();
            minimumCanvasSize.setSize(value);
            updateEffectiveMaximumCanvasSize(old);
            revalidateCanvas(); // always needed here, may grow
        }
    }

    /** {@inheritDoc} */
    public Dimension getMaximumCanvasSize() {
        return (Dimension) maximumCanvasSize.clone();
    }

    /** {@inheritDoc} */
    public void setMaximumCanvasSize(final Dimension value) {
        if (!value.equals(maximumCanvasSize)) {
            final Dimension old = getEffectiveMaximumCanvasSize();
            maximumCanvasSize.setSize(value);
            updateEffectiveMaximumCanvasSize(old);
        }
    }

    /**
     * The effective maximum canvas size. This is equal to the maximum canvas size,
     * but with a lower bound of minimum canvas size for width and height.
     *
     * @return the effective maximum canvas size.
     */
    protected Dimension getEffectiveMaximumCanvasSize() {
        final Dimension result = new Dimension(maximumCanvasSize);

        result.width = Math.max(result.width, minimumCanvasSize.width);
        result.height = Math.max(result.height, minimumCanvasSize.height);

        return result;
    }

    private void updateEffectiveMaximumCanvasSize(final Dimension oldValue) {
        // The offscreen image size is currently always the effective maximum canvas size,
        // so the image may need to be recreated now.
        if (!oldValue.equals(getEffectiveMaximumCanvasSize())) {
            rebuildImage();
            revalidateCanvas();
            repaint();
        }
    }

    /**
     * Point 'p' in component coordinates converted to graph coordinates.
     * At the moment, only translated by canvas origin.
     *
     * @param p Point in component coordinates.
     * @return Point in graph coordinates.
     */
    protected Point toGraphCoordinates(final Point p) {
        final Insets n = getInsets();

        return new Point(p.x - n.left, p.y - n.top);
    }

    /**
     * Point 'p' in graph coordinates converted to component coordinates.
     * At the moment, only translated by canvas origin.
     *
     * @param p Point in graph coordinates.
     * @return Point in component coordinates.
     */
    protected Point toComponentCoordinates(final Point p) {
        final Insets n = getInsets();

        return new Point(p.x + n.left, p.y + n.top);
    }

    /**
     * A Rectangle in graph coordinates converted to component coordinates.
     * At the moment, only translated by canvas origin.
     *
     * @param r Rectangle in graph coordinates.
     * @return Rectangle in component coordinates.
     */
    protected Rectangle toComponentCoordinates(final Rectangle r) {
        final Rectangle result = new Rectangle(r);

        result.setLocation(toComponentCoordinates(r.getLocation()));

        return result;
    }

    /**
     * Determines the bounds of the given object on the screen.
     *
     * @param o the JaxoObject.
     * @return Rectangle
     */
    protected Rectangle getScreenBounds(final JaxoObject o) {
        final Rectangle bounds = toComponentCoordinates(o.getBounds());

        final Point p = getLocationOnScreen();

        return new Rectangle(p.x + bounds.x, p.y + bounds.y, bounds.width, bounds.height);
    }

    /** {@inheritDoc} */
    public JaxoZoom getZoom() {
        return zoom;
    }

    /**
     * Grid to be used.
     * A non-null grid is painted and potentially used for snapping points.
     * If grid properties change, the canvas automatically updates and repaints itself.
     *
     * @return The grid currently used by this canvas. May be null.
     */
    // NOTE: from outside, the grid should be retrieved from the tab, not the canvas!
    protected JaxoPaintableGrid getGrid() {
        return grid;
    }

    /** {@inheritDoc} */
    public void setGrid(final JaxoPaintableGrid value) {
        if (value != grid) {
            if (grid != null) {
                grid.removeChangeListener(gridListener);
            }

            grid = value;

            if (grid != null) {
                grid.addChangeListener(gridListener);
            }

            markBackgroundInvalid();
        }
    }

    /**
     * Set both graph and grid. This is an optimization to avoid
     * painting to the background image twice (because that is done
     * synchronously).
     * @param newGraph The new graph.
     * @param newGrid The new grid.
     */
    public void setCanvasGraphAndGrid(final JaxoGraph newGraph, final JaxoPaintableGrid newGrid) {
        setCanvasGraph(newGraph);

        if (grid == null) {
            setGrid(newGrid);
        } else {
            if (newGrid != grid) {
                setGrid(newGrid);
                revalidateCanvas();
            }
        }
    }

    /**
     * Determines if points are snapped to the grid.
     *
     * @return True if snapping is currently enabled.
     */
    private boolean isSnappingToGrid() {
        if (grid != null) {
            return grid.isSnapping();
        }

        return false;
    }

    /**
     * Determines if objects are painted with antialiasing turned on.
     * This does not apply to handles.
     *
     * @return True if antialising is enabled.
     */
    protected boolean isAntialiasEnabled() {
        return antialiasEnabled;
    }

    /**
     * Sets the antialias property.
     *
     * @param value True if antialising should be enabled.
     */
    public void setAntialiasEnabled(final boolean value) {
        if (antialiasEnabled != value) {
            antialiasEnabled = value;
            markImageInvalid();
        }
    }

    /////////////////////////////////////////////////////////////////////
    // Canonical implementation of Scrollable, except for unit increment.
    /////////////////////////////////////////////////////////////////////

    /**
     * Return the preferred size of the viewport.
     *
     * @return The preferred size of the viewport.
     */
    public Dimension getPreferredScrollableViewportSize() {
        final Dimension result = getMinimumCanvasSize();

        JaxoGeometry.grow(result, getInsets());

        return result;
    }

    /**
     * Return the unit increment for scrolling.
     *
     * @param visibleRect the visible view area.
     * @param orientation Either SwingConstants.VERTICAL
     *      or SwingConstants.HORIZONTAL.
     * @param direction less than zero to scroll up/left,
     *      greater than zero for down/right.
     * @return Returns 6.
     */
    public int getScrollableUnitIncrement(final Rectangle visibleRect,
            final int orientation, final int direction) {
        return 6; // make configurable?
    }

    /**
     * Return the unit increment for block scrolling.
     *
     * @param visibleRect the visible view area.
     * @param orientation Either SwingConstants.VERTICAL
     *      or SwingConstants.HORIZONTAL.
     * @param direction less than zero to scroll up/left,
     *      greater than zero for down/right.
     * @return Returns the width/height of visibleRect depending on orientation.
     */
    public int getScrollableBlockIncrement(final Rectangle visibleRect,
            final int orientation, final int direction) {
        return (orientation == SwingConstants.HORIZONTAL) ? visibleRect.width
                                                          : visibleRect.height;
    }

    /**
     * Return true if a viewport should force the width of the scrollable.
     *
     * @return true if the parent's width is larger than
     *      the width of the preferred size.
     */
    public boolean getScrollableTracksViewportWidth() {
        return getParent() instanceof JViewport
        && (getParent().getWidth() > getPreferredSize().width);
    }

    /**
     * Return true if a viewport should force the height of the scrollable.
     *
     * @return true if the parent's height is larger than
     *      the height of the preferred size.
     */
    public boolean getScrollableTracksViewportHeight() {
        return getParent() instanceof JViewport
        && (getParent().getHeight() > getPreferredSize().height);
    }

    /**
     * Check if the system clipboard contains a JaxoGraph.
     *
     * @return true if the system clipboard contains a JaxoGraph, false otherwise.
     */
    protected boolean canPasteGraphFromSystemClipboard() {
        return clipboard.canPasteGraphFromSystemClipboard();
    }

    /**
     * Graph on the system clipboard.
     *
     * @return the current graph on the system clipboard, or null if none.
     */
    protected JaxoGraph getSystemClipboardGraph() {
        return clipboard.getSystemClipboardGraph();
    }

    /**
     * Make 'g' the contents of the system clipboard.
     *
     * @param g the graph to put on the clipboard.
     */
    protected void putToSystemClipboard(final JaxoGraph g) {
        clipboard.putToSystemClipboard(g);
    }

    /** {@inheritDoc} */
    public JaxoGraph getClipboard() {
        return new JaxoGraph(canvasGraph.getCopyOfMarkedObjects());
    }

    /**
     * Mark background or grid invalid (but not the graph/objects).
     */
    protected void markBackgroundInvalid() {
        markImageInvalid();
    }

    /**
     * Mark the whole image as invalid.
     * Currently this repaints everything at once, could be changed to defer until later.
     */
    public void markImageInvalid() {
        markImageInvalid(null);
    }

    /**
     * Paint background (unless covered by grid), and grid (if on).
     * This method may change the graphics clip and color.
     *
     * @param g the graphics context to paint to.
     * @param rect the rectangle to paint.
     */
    protected void paintBackgroundAndGrid(final Graphics2D g, final Rectangle rect) {
        g.clipRect(rect.x, rect.y, rect.width, rect.height);

        if ((grid == null) || (grid.getTransparency() != Transparency.OPAQUE)) {
            g.setColor(getCanvasBackground());

            g.fillRect(rect.x, rect.y, rect.width, rect.height);
        }

        if (grid != null) {
            grid.setCanvasSize(getEffectiveMaximumCanvasSize());
            grid.paint(g);
        }
    }

    /**
     * Snap the given point to the current grid. If the current grid is null, does nothing.
     *
     * @param p the point to snap.
     */
    protected void snapPoint(final Point p) {
        if (isSnappingToGrid()) { // this implies grid != null
            grid.snapPoint(p);
        }
    }

    /**
     * Moves the given JaxoObject so that its first point is snapped to the current grid.
     * If the current grid is null, does nothing.
     *
     * @param o the JaxoObject to snap.
     */
    protected void snapObject(final JaxoObject o) {
        // don't snap groups because the snap points are calculated from the
        // bounding box, which leads to an offset for objects inside groups
        // need to solve this more generally?
        if (isSnappingToGrid() && !(o instanceof JaxoGroup)) { // this implies grid != null
            final Point p = new Point(o.getX(), o.getY());
            final Point q = new Point(p);

            grid.snapPoint(q);

            o.moveBy(q.x - p.x, q.y - p.y);
        }
    }

    private ChangeListener getNewGridListener() {
        return new ChangeListener() {
                public void stateChanged(final ChangeEvent e) {
                    markBackgroundInvalid();
                }
            };
    }

    /** Updates the language on any localized sub-components. */
    public void updateLanguage() {
        // try to pull these out of canvas!
        fboxPopup.updateLanguage();
        colorChooser.updateLanguage();
    }

    private void resetColorChoser() {
        colorChooser = new JaxoColorChooser(this.asComponent());
        colorChooser.setPermanent(true);
    }

    /**
     * Repaint the given Rectangle in graph coordinates.
     * Currently these are always the same as component coordinates.
     *
     * @param r the Rectangle to repaint.
     */
    protected void repaintBoundingBox(final Rectangle2D r) {
        final Rectangle s = toComponentCoordinates(r.getBounds());

        JaxoGeometry.grow(s, COMPONENT_REPAINT_PADDING);

        repaint(s);
    }

    /**
     * Brings up a ColorChooser panel for the given object.
     *
     * @param ob the object to edit.
     * @return true if the color of the object has been changed, false if it is unchanged.
     */
    protected boolean showColorPanel(final JaxoObject ob) {
        final Rectangle screenBounds = getScreenBounds(ob);
        final Location location = new Location.RelativeToAvoiding(asComponent(), screenBounds);
        final int colorMode = JaxoPrefs.getIntPref(JaxoPrefs.PREF_COLORSPACE);

        if (ob instanceof JaxoFillColorObject
                && ((JaxoFillColorObject) ob).isFilled()) {
            final JaxoFillColorObject fillObject = (JaxoFillColorObject) ob;

            colorChooser.setMode(colorMode == JaxoColor.ALL_COLORS_MODE
            ? JaxoColor.ALL_COLORS_MODE : JaxoColor.JAXO_COLORS_MODE);

            colorChooser.setOptional(false);
            colorChooser.setColor(fillObject.getFillColor());

            colorChooser.addChangeListener(new ColorChooserListener(ob) {
                @Override
                    protected void setColor(final Color c) {
                        fillObject.setFillColor(c);
                    }
                });
            colorChooser.show(location);

            if (colorChooser.hasChanged()
                    && JaxoColor.isGrayScale(colorChooser.getColor())) {
                ob.setColor(JaxoColor.BLACK);
            }
        } else if (ob instanceof JaxoGroup) {
            colorChooser.setMode(colorMode == JaxoColor.ALL_COLORS_MODE
            ? JaxoColor.ALL_COLORS_MODE : JaxoColor.JAXO_COLORS_NO_GRAYSCALES_MODE);

            final JaxoGroup g = (JaxoGroup) ob;
            final JaxoList<JaxoObject> oldContents = g.getObjectList();
            final JaxoList<JaxoObject> tmpContents = oldContents.copyOf();

            colorChooser.setOptionalColor(g.getColor());

            colorChooser.addChangeListener(new ColorChooserListener(g) {
                @Override
                    protected void setColor(final Color c) {
                        if (c == null) {
                            g.setObjectList(oldContents);
                        } else {
                            g.setObjectList(tmpContents);
                            g.setColor(c);
                        }
                    }
                });
            colorChooser.show(location);

            // Always restore real contents
            g.setObjectList(oldContents);

            if (colorChooser.hasChanged() && colorChooser.getColor() != null) {
                // /Now/ setColor also for contents
                g.setColor(colorChooser.getColor());
            }
        } else {
            colorChooser.setMode(colorMode == JaxoColor.ALL_COLORS_MODE
            ? JaxoColor.ALL_COLORS_MODE
            : ((ob instanceof JaxoPSText)
                ? JaxoColor.JAXO_COLORS_MODE
                : JaxoColor.JAXO_COLORS_NO_GRAYSCALES_MODE));

            colorChooser.setOptional(false);
            colorChooser.setColor(ob.getColor());

            colorChooser.addChangeListener(new ColorChooserListener(ob));
            colorChooser.show(location);
        }

        return colorChooser.hasChanged();
    }

    /**
     * ColorChooser listeners calls setColor for every ChangeEvent and then repaints the object
     * given at construction time. It removes itself automatically once the color choosing is done.
     */
    private class ColorChooserListener implements ChangeListener {
        private final JaxoObject object;

        ColorChooserListener(final JaxoObject o) {
            object = o;
        }

        protected final JaxoObject object() {
            return object;
        }

        public void stateChanged(final ChangeEvent e) {
            setColor(colorChooser.getColor());

            // Assume that the bounding box is valid
            // This is safe, as the bounding box does not change when changing the color
            if (HOVERING_EDITED_OBJECTS) {
                repaintBoundingBox(object.getBounds());
            } else {
                markImageInvalid(object.getBounds());
            }

            if (!colorChooser.isAdjusting()) {
                colorChooser.removeChangeListener(this);
            }
        }

        protected void setColor(final Color c) {
            object.setColor(c);
        }
    }

    /**
     * Brings up the faint-box popup panel for the current selection.
     *
     * @param location the location of the popup panel.
     * @return true if the selection panel was actually shown. This is the case if either
     * the graph contained marked objects (i.e. the selection is not empty) or there are
     * currently objects on the clipboard to paste. Returns false if none of this is the case.
     */
    protected boolean showSelectionPanel(final Point location) {
        // Show the corresponding possible actions for the marked objects
        final boolean canPaste = canPasteGraphFromSystemClipboard();

        if (getCanvasGraph().containsMarkedObjects()) {
            fboxPopup.setMenuItemEnabled(JaxoFBoxPopupMenu.PASTE, canPaste);
            fboxPopup.setMenuItemEnabled(JaxoFBoxPopupMenu.UNGROUP,
                getCanvasGraph().containsMarkedGroups());
            fboxPopup.setMenuItemEnabled(JaxoFBoxPopupMenu.GROUP,
                getCanvasGraph().hasMoreMarkedObjectsThan(1));

            fboxPopup.normalPopup();
            fboxPopup.show(this, location.x, location.y);
        } else if (canPaste) {
            fboxPopup.setMenuItemEnabled(JaxoFBoxPopupMenu.PASTE, true);
            fboxPopup.onlyPastePopup();
            fboxPopup.show(this, location.x, location.y);
        } else {
            // Otherwise called later by popup
            return false;
        }

        return true;
    }

    /**
     * Bring up an EditPanel to edit properties of the given JaxoObject.
     *
     * @param o the object to edit.
     * @return true if the object has been modified, false if the object was unchanged.
     */
    protected boolean showEditPanel(final JaxoObject o) {
        final int old = getHandlePaintMode();
        setHandlePaintMode(HANDLE_PAINT_SELECTION);

        final JaxoObjectEditPanel p = new JaxoOptionsPanel(o);
        p.addChangeListener(new EditPanelListener());

        final Rectangle screenBounds = getScreenBounds(o);

        final Location l = new Location.RelativeToAvoiding(asComponent(), screenBounds);

        p.show(asComponent(), l);
        setHandlePaintMode(old);

        return p.hasChanged();
    }

    private class EditPanelListener implements ChangeListener {

        public void stateChanged(final ChangeEvent evt) {
            repaintObject();
        }

        private void repaintObject() {
            /** Does not work unless bounding box is up-to-date
            Rectangle bounds = oldBounds.union(newBounds);
            if (HOVERING_EDITED_OBJECTS) {
                repaintBoundingBox(bounds);
            } else {
                markImageInvalid(bounds);
            }
            */

            // Legacy
            if (HOVERING_EDITED_OBJECTS) {
                repaint();
            } else {
                // quite inefficient
                markImageInvalid();
            }
        }
    }

    /**
     * Resets the preferred size of the canvas.
     */
    protected abstract void revalidateCanvas();

    /**
     * Build a new off-screen image. This should only be needed for resizing the canvas.
     */
    protected abstract void rebuildImage();

    /**
     * Deletes all 'marked' objects from the drawing area and the current canvas graph.
     */
    protected abstract void deleteMarkedObjects();

    /**
     * Resets canvas parameters for the given mode.
     * Sets the cursor, and determines whether to draw visualAid and handles.
     *
     * @param mode The mode to adjust to.
     */
    protected abstract void updateMode(int mode);
}
