/*
* @(#)AbstractFigure.java
*
* Copyright (c) 1996-2010 The authors and contributors of JHotDraw.
* You may not use, copy or modify this file, except in compliance with the
* accompanying license terms.
*/
package org.jhotdraw.draw;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.jhotdraw.draw.tool.Tool;
import org.jhotdraw.draw.connector.Connector;
import org.jhotdraw.draw.connector.ChopRectangleConnector;
import org.jhotdraw.draw.event.SetBoundsEdit;
import org.jhotdraw.draw.event.FigureListener;
import org.jhotdraw.draw.event.FigureEvent;
import org.jhotdraw.beans.AbstractBean;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
import javax.swing.undo.*;
import org.jhotdraw.draw.handle.BoundsOutlineHandle;
import org.jhotdraw.draw.handle.Handle;
import org.jhotdraw.draw.handle.ResizeHandleKit;
import org.jhotdraw.geom.*;
/**
* This abstract class can be extended to implement a {@link Figure}.
*
*
* @author Werner Randelshofer
* @version $Id$
*/
public abstract class AbstractFigure
extends AbstractBean
implements Figure {
private static final long serialVersionUID = 1L;
protected EventListenerList listenerList = new EventListenerList();
@Nullable private Drawing drawing;
private boolean isSelectable = true;
private boolean isRemovable = true;
private boolean isVisible = true;
private boolean isTransformable = true;
private boolean isConnectable=true;
/**
* This variable is used to prevent endless change loops.
* We increase its value on each invocation of willChange() and
* decrease it on each invocation of changed().
*/
protected int changingDepth = 0;
/** Creates a new instance. */
public AbstractFigure() {
}
// DRAWING
// SHAPE AND BOUNDS
// ATTRIBUTES
// EDITING
// CONNECTING
// COMPOSITE FIGURES
// CLONING
// EVENT HANDLING
@Override
public void addFigureListener(FigureListener l) {
listenerList.add(FigureListener.class, l);
}
@Override
public void removeFigureListener(FigureListener l) {
listenerList.remove(FigureListener.class, l);
}
@Override
public void addNotify(Drawing d) {
this.drawing = d;
fireFigureAdded();
}
@Override
public void removeNotify(Drawing d) {
fireFigureRemoved();
this.drawing = null;
}
protected Drawing getDrawing() {
return drawing;
}
protected Object getLock() {
return (getDrawing() == null) ? this : getDrawing().getLock();
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
public void fireAreaInvalidated() {
fireAreaInvalidated(getDrawingArea());
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireAreaInvalidated(Rectangle2D.Double invalidatedArea) {
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, invalidatedArea);
}
((FigureListener) listeners[i + 1]).areaInvalidated(event);
}
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireAreaInvalidated(FigureEvent event) {
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
((FigureListener) listeners[i + 1]).areaInvalidated(event);
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureRequestRemove() {
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, getBounds());
}
((FigureListener) listeners[i + 1]).figureRequestRemove(event);
}
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureAdded() {
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, getBounds());
}
((FigureListener) listeners[i + 1]).figureAdded(event);
}
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureRemoved() {
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, getBounds());
}
((FigureListener) listeners[i + 1]).figureRemoved(event);
}
}
}
}
public void fireFigureChanged() {
fireFigureChanged(getDrawingArea());
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureChanged(Rectangle2D.Double changedArea) {
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, changedArea);
}
((FigureListener) listeners[i + 1]).figureChanged(event);
}
}
}
}
protected void fireFigureChanged(FigureEvent event) {
if (listenerList.getListenerCount() > 0) {
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
((FigureListener) listeners[i + 1]).figureChanged(event);
}
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected <T> void fireAttributeChanged(AttributeKey<T> attribute, T oldValue, T newValue) {
if (listenerList.getListenerCount() > 0 &&
(oldValue == null || newValue == null || !oldValue.equals(newValue))) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, attribute, oldValue, newValue);
}
((FigureListener) listeners[i + 1]).attributeChanged(event);
}
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureHandlesChanged() {
Rectangle2D.Double changedArea = getDrawingArea();
if (listenerList.getListenerCount() > 0) {
FigureEvent event = null;
// Notify all listeners that have registered interest for
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == FigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new FigureEvent(this, changedArea);
}
((FigureListener) listeners[i + 1]).figureHandlesChanged(event);
}
}
}
}
/**
* Notify all UndoableEditListener of the Drawing, to which this Figure has
* been added to. If this Figure is not part of a Drawing, the event is
* lost.
*/
protected void fireUndoableEditHappened(UndoableEdit edit) {
if (getDrawing() != null) {
getDrawing().fireUndoableEditHappened(edit);
}
}
/*
public Set createHandles() {
return new HashSet();
}
*/
@Override
public AbstractFigure clone() {
AbstractFigure that = (AbstractFigure) super.clone();
that.listenerList = new EventListenerList();
that.drawing = null; // Clones need to be explictly added to a drawing
return that;
}
@Override
public void remap(Map<Figure, Figure> oldToNew, boolean disconnectIfNotInMap) {
}
@Override
public Collection<Handle> createHandles(int detailLevel) {
LinkedList<Handle> handles = new LinkedList<Handle>();
switch (detailLevel) {
case -1:
handles.add(new BoundsOutlineHandle(this,false,true));
break;
case 0:
ResizeHandleKit.addResizeHandles(this, handles);
break;
}
return handles;
}
@Override
public Cursor getCursor(Point2D.Double p) {
if (contains(p)) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else {
return Cursor.getDefaultCursor();
}
}
public final void setBounds(Rectangle2D.Double bounds) {
setBounds(new Point2D.Double(bounds.x, bounds.y), new Point2D.Double(bounds.x + bounds.width, bounds.y + bounds.height));
}
@Override
public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
Point2D.Double oldAnchor = getStartPoint();
Point2D.Double oldLead = getEndPoint();
if (!oldAnchor.equals(anchor) || !oldLead.equals(lead)) {
willChange();
setBounds(anchor, lead);
changed();
fireUndoableEditHappened(new SetBoundsEdit(this, oldAnchor, oldLead, anchor, lead));
}
}
/**
* Invalidates cached data of the Figure.
* This method must execute fast, because it can be called very often.
*/
protected void invalidate() {
}
protected boolean isChanging() {
return changingDepth != 0;
}
protected int getChangingDepth() {
return changingDepth;
}
/**
* Informs that a figure is about to change something that
* affects the contents of its display box.
*/
@Override
public void willChange() {
if (changingDepth == 0) {
fireAreaInvalidated();
invalidate();
}
changingDepth++;
}
protected void validate() {
}
/**
* Informs that a figure changed the area of its display box.
*/
@Override
public void changed() {
if (changingDepth == 1) {
validate();
fireFigureChanged(getDrawingArea());
} else if (changingDepth < 0) {
throw new InternalError("changed was called without a prior call to willChange. "+changingDepth);
}
changingDepth--;
}
/**
* Returns the Figures connector for the specified location.
* By default a ChopBoxConnector is returned.
*
*
* @see ChopRectangleConnector
*/
@Override
public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) {
return new ChopRectangleConnector(this);
}
@Override
public boolean includes(Figure figure) {
return figure == this;
}
@Override
@Nullable public Figure findFigureInside(Point2D.Double p) {
return (contains(p)) ? this : null;
}
@Override
public Connector findCompatibleConnector(Connector c, boolean isStart) {
return new ChopRectangleConnector(this);
}
/**
* Returns a collection of actions which are presented to the user
* in a popup menu.
* <p>The collection may contain null entries. These entries are used
* interpreted as separators in the popup menu.
*/
@Override
public Collection<Action> getActions(Point2D.Double p) {
return Collections.emptyList();
}
/**
* Returns a specialized tool for the given coordinate.
* <p>Returns null, if no specialized tool is available.
*/
@Override
public Tool getTool(Point2D.Double p) {
return null;
}
/**
* Handles a mouse click.
*/
@Override
public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) {
return false;
}
@Override
public boolean handleDrop(Point2D.Double p, Collection<Figure> droppedFigures, DrawingView view) {
return false;
}
@Override
public Point2D.Double getEndPoint() {
Rectangle2D.Double r = getBounds();
return new Point2D.Double(r.x + r.width, r.y + r.height);
}
@Override
public Point2D.Double getStartPoint() {
Rectangle2D.Double r = getBounds();
return new Point2D.Double(r.x, r.y);
}
/*
public Rectangle2D.Double getHitBounds() {
return getBounds();
}
*/
@Override
public Dimension2DDouble getPreferredSize() {
Rectangle2D.Double r = getBounds();
return new Dimension2DDouble(r.width, r.height);
}
/**
* Checks whether this figure is connectable. By default
* {@code AbstractFigure} can be connected.
*/
@Override
public boolean isConnectable() {
return isConnectable;
}
public void setConnectable(boolean newValue) {
boolean oldValue = isConnectable;
isConnectable = newValue;
firePropertyChange(CONNECTABLE_PROPERTY, oldValue, newValue);
}
/**
* Checks whether this figure is selectable. By default
* {@code AbstractFigure} can be selected.
*/
@Override
public boolean isSelectable() {
return isSelectable;
}
public void setSelectable(boolean newValue) {
boolean oldValue = isSelectable;
isSelectable = newValue;
firePropertyChange(SELECTABLE_PROPERTY, oldValue, newValue);
}
/**
* Checks whether this figure is removable. By default
* {@code AbstractFigure} can be removed.
*/
@Override
public boolean isRemovable() {
return isRemovable;
}
public void setRemovable(boolean newValue) {
boolean oldValue = isRemovable;
isRemovable = newValue;
firePropertyChange(REMOVABLE_PROPERTY, oldValue, newValue);
}
/**
* Checks whether this figure is transformable. By default
* {@code AbstractFigure} can be transformed.
*/
@Override
public boolean isTransformable() {
return isTransformable;
}
public void setTransformable(boolean newValue) {
boolean oldValue = isTransformable;
isTransformable = newValue;
firePropertyChange(TRANSFORMABLE_PROPERTY, oldValue, newValue);
}
/**
* Checks whether this figure is visible. By default
* {@code AbstractFigure} is visible.
*/
@Override
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean newValue) {
if (newValue != isVisible) {
willChange();
isVisible = newValue;
changed();
}
}
@Override
public Collection<Figure> getDecomposition() {
LinkedList<Figure> list = new LinkedList<Figure>();
list.add(this);
return list;
}
protected FontRenderContext getFontRenderContext() {
FontRenderContext frc = null;
if (frc == null) {
frc = new FontRenderContext(new AffineTransform(), true, true);
}
return frc;
}
@Override
public void requestRemove() {
fireFigureRequestRemove();
}
/** AbstractFigure always returns 0.
* Override this method if your figure needs to be on a different layer.
*/
@Override
public int getLayer() {
return 0;
}
@Override
public String getToolTipText(Point2D.Double p) {
return null;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(getClass().getName().substring(getClass().getName().lastIndexOf('.') + 1));
buf.append('@');
buf.append(hashCode());
return buf.toString();
}
@Override
public Collection<Connector> getConnectors(ConnectionFigure prototype) {
LinkedList<Connector> connectors = new LinkedList<Connector>();
connectors.add(new ChopRectangleConnector(this));
return connectors;
}
}