/*
* @(#)AbstractCompositeFigure.java
*
* Copyright (c) 2007-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 org.jhotdraw.geom.Dimension2DDouble;
import javax.annotation.Nullable;
import org.jhotdraw.draw.layouter.Layouter;
import org.jhotdraw.draw.event.FigureAdapter;
import org.jhotdraw.draw.event.FigureEvent;
import org.jhotdraw.draw.event.CompositeFigureEvent;
import org.jhotdraw.draw.event.CompositeFigureListener;
import java.io.IOException;
import org.jhotdraw.util.*;
import java.awt.*;
import java.awt.geom.*;
import java.io.Serializable;
import java.util.*;
import javax.swing.event.*;
import org.jhotdraw.draw.handle.BoundsOutlineHandle;
import org.jhotdraw.draw.handle.Handle;
import org.jhotdraw.draw.handle.TransformHandleKit;
import org.jhotdraw.xml.DOMInput;
import org.jhotdraw.xml.DOMOutput;
import org.jhotdraw.xml.DOMStorable;
import static org.jhotdraw.draw.AttributeKeys.*;
/**
* This abstract class can be extended to implement a {@link CompositeFigure}.
* AbstractCompositeFigure.
*
* @author Werner Randelshofer
* @version $Id$
*/
public abstract class AbstractCompositeFigure
extends AbstractFigure
implements CompositeFigure, DOMStorable {
private static final long serialVersionUID = 1L;
/**
* A Layouter determines how the children of the CompositeFigure
* are laid out graphically.
*/
protected Layouter layouter;
/**
* The children that this figure is composed of
*
* @see #add
* @see #removeChild
*/
protected ArrayList<Figure> children = new ArrayList<>();
/**
* Caches the drawing area to improve the performance of method {@link #getDrawingArea}.
*/
@Nullable protected transient Rectangle2D.Double cachedDrawingArea;
/**
* Caches the bounds to improve the performance of method {@link #getBounds}.
*/
@Nullable protected transient Rectangle2D.Double cachedBounds;
/**
* Handles figure changes in the children.
*/
protected EventHandler eventHandler;
protected class EventHandler extends FigureAdapter implements UndoableEditListener, Serializable {
private static final long serialVersionUID = 1L;
@Override
public void figureRequestRemove(FigureEvent e) {
remove(e.getFigure());
}
@Override
public void figureChanged(FigureEvent e) {
if (!isChanging()) {
Rectangle2D.Double invalidatedArea = getDrawingArea();
invalidatedArea.add(e.getInvalidatedArea());
// We call invalidate/validate here, because we must layout
// the figure again.
invalidate();
validate();
// Forward the figureChanged event to listeners on AbstractCompositeFigure.
invalidatedArea.add(getDrawingArea());
fireFigureChanged(invalidatedArea);
}
}
@Override
public void areaInvalidated(FigureEvent e) {
fireAreaInvalidated(e);
}
@Override
public void undoableEditHappened(UndoableEditEvent e) {
fireUndoableEditHappened(e.getEdit());
}
@Override
public void attributeChanged(FigureEvent e) {
invalidate();
}
@Override
public void figureAdded(FigureEvent e) {
invalidate();
}
@Override
public void figureRemoved(FigureEvent e) {
invalidate();
}
}
public AbstractCompositeFigure() {
eventHandler = createEventHandler();
}
@Override
public Collection<Handle> createHandles(int detailLevel) {
LinkedList<Handle> handles = new LinkedList<>();
if (detailLevel == 0) {
handles.add(new BoundsOutlineHandle(this, true, false));
TransformHandleKit.addScaleMoveTransformHandles(this, handles);
}
return handles;
}
protected EventHandler createEventHandler() {
return new EventHandler();
}
@Override
public boolean add(Figure figure) {
add(getChildCount(), figure);
return true;
}
@Override
public void add(int index, Figure figure) {
basicAdd(index, figure);
if (getDrawing() != null) {
figure.addNotify(getDrawing());
}
fireFigureAdded(figure, index);
invalidate();
}
public void addAll(Collection<? extends Figure> figures) {
addAll(getChildCount(), figures);
}
public final void addAll(int index, Collection<? extends Figure> figures) {
for (Figure f : figures) {
basicAdd(index++, f);
if (getDrawing() != null) {
f.addNotify(getDrawing());
}
fireFigureAdded(f, index);
}
invalidate();
}
@Override
public void basicAdd(Figure figure) {
basicAdd(getChildCount(), figure);
}
public void basicAddAll(int index, Collection<? extends Figure> newFigures) {
for (Figure f : newFigures) {
basicAdd(index++, f);
}
}
@Override
public void addNotify(Drawing drawing) {
super.addNotify(drawing);
for (Figure child : getChildren()) {
child.addNotify(drawing);
}
}
@Override
public void removeNotify(Drawing drawing) {
super.removeNotify(drawing);
// Copy children collection to avoid concurrent modification exception
for (Figure child : new LinkedList<>(getChildren())) {
child.removeNotify(drawing);
}
}
@Override
public boolean remove(final Figure figure) {
int index = children.indexOf(figure);
if (index == -1) {
return false;
} else {
basicRemoveChild(index);
if (getDrawing() != null) {
figure.removeNotify(getDrawing());
}
fireFigureRemoved(figure, index);
return true;
}
}
@Override
public Figure removeChild(int index) {
Figure removed = basicRemoveChild(index);
if (getDrawing() != null) {
removed.removeNotify(getDrawing());
}
return removed;
}
/**
* Removes all specified children.
*
* @see #add
*/
public void removeAll(Collection<? extends Figure> figures) {
willChange();
for (Figure f : new LinkedList<Figure>(figures)) {
remove(f);
}
changed();
}
/**
* Removes all children.
*
* @see #add
*/
@Override
public void removeAllChildren() {
removeAll(getChildren());
}
/**
* Removes all children.
*
* @see #add
*/
@Override
public void basicRemoveAllChildren() {
for (Figure f : new LinkedList<>(getChildren())) {
basicRemove(f);
}
}
/**
* Removes all children.
*
* @see #add
*/
public void basicRemoveAll(Collection<? extends Figure> figures) {
for (Figure f : figures) {
basicRemove(f);
}
}
/**
* Sends a figure to the back of the composite figure.
*
* @param figure that is part of this composite figure
*/
public void sendToBack(Figure figure) {
if (basicRemove(figure) != -1) {
basicAdd(0, figure);
fireAreaInvalidated(figure.getDrawingArea());
}
}
/**
* Brings a figure to the front of the drawing.
*
* @param figure that is part of the drawing
*/
public void bringToFront(Figure figure) {
if (basicRemove(figure) != -1) {
basicAdd(figure);
fireAreaInvalidated(figure.getDrawingArea());
}
}
/**
* Transforms the figure.
*/
@Override
public void transform(AffineTransform tx) {
for (Figure f : getChildren()) {
f.transform(tx);
}
invalidate();
//invalidate();
}
@Override
public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
Rectangle2D.Double oldBounds = getBounds();
Rectangle2D.Double newBounds = new Rectangle2D.Double(
Math.min(anchor.x, lead.x),
Math.min(anchor.y, lead.y),
Math.abs(anchor.x - lead.x),
Math.abs(anchor.y - lead.y));
double sx = newBounds.width / oldBounds.width;
double sy = newBounds.height / oldBounds.height;
AffineTransform tx = new AffineTransform();
tx.translate(-oldBounds.x, -oldBounds.y);
if (!Double.isNaN(sx) && !Double.isNaN(sy)
&& !Double.isInfinite(sx) && !Double.isInfinite(sy)
&& (sx != 1d || sy != 1d)
&& !(sx < 0.0001) && !(sy < 0.0001)) {
transform(tx);
tx.setToIdentity();
tx.scale(sx, sy);
transform(tx);
tx.setToIdentity();
}
tx.translate(newBounds.x, newBounds.y);
transform(tx);
}
/**
* Returns an iterator to iterate in
* Z-order front to back over the children.
*/
public java.util.List<Figure> getChildrenFrontToBack() {
return children.size() == 0 ? new LinkedList<>() : new ReversedList<>(getChildren());
}
@Override
public <T> void set(AttributeKey<T> key, T value) {
for (Figure child : getChildren()) {
child.set(key, value);
}
invalidate();
}
@Override
public <T> T get(AttributeKey<T> name) {
return null;
}
@Override
public Map<AttributeKey<?>, Object> getAttributes() {
return new HashMap<>();
}
@Override
public Object getAttributesRestoreData() {
LinkedList<Object> data = new LinkedList<>();
for (Figure child : getChildren()) {
data.add(child.getAttributesRestoreData());
}
return data;
}
@Override
public void restoreAttributesTo(Object newData) {
@SuppressWarnings("unchecked")
Iterator<Object> data = ((LinkedList<Object>) newData).iterator();
for (Figure child : getChildren()) {
child.restoreAttributesTo(data.next());
}
}
@Override
public boolean contains(Figure f) {
return children.contains(f);
}
@Override
public boolean contains(Point2D.Double p) {
if (getDrawingArea().contains(p)) {
if (get(TRANSFORM) != null) {
try {
p = (Point2D.Double) get(TRANSFORM).inverseTransform(p, new Point2D.Double());
} catch (NoninvertibleTransformException ex) {
InternalError error = new InternalError(ex.getMessage());
error.initCause(ex);
throw error;
}
}
for (Figure child : getChildren()) {
if (child.isVisible() && child.contains(p)) {
return true;
}
}
}
return false;
}
@Override
@Nullable public Figure findFigureInside(Point2D.Double p) {
if (getDrawingArea().contains(p)) {
Figure found = null;
for (Figure child : getChildrenFrontToBack()) {
if (child.isVisible()) {
found = child.findFigureInside(p);
if (found != null) {
return found;
}
}
}
}
return null;
}
@Nullable public Figure findChild(Point2D.Double p) {
if (getBounds().contains(p)) {
Figure found = null;
for (Figure child : getChildrenFrontToBack()) {
if (child.isVisible() && child.contains(p)) {
return child;
}
}
}
return null;
}
public int findChildIndex(Point2D.Double p) {
Figure child = findChild(p);
return (child == null) ? -1 : children.indexOf(child);
}
/**
* Get a Layouter object which encapsulated a layout
* algorithm for this figure. Typically, a Layouter
* accesses the child components of this figure and arranges
* their graphical presentation.
*
*
* @return layout strategy used by this figure
*/
@Override
public Layouter getLayouter() {
return layouter;
}
/**
* A layout algorithm is used to define how the child components
* should be laid out in relation to each other. The task for
* layouting the child components for presentation is delegated
* to a Layouter which can be plugged in at runtime.
*/
@Override
public void layout() {
// Note: We increase and below decrease the changing depth here,
// because we want to ignore change events from our children
// why we lay them out.
changingDepth++;
for (Figure child : getChildren()) {
if (child instanceof CompositeFigure) {
CompositeFigure cf = (CompositeFigure) child;
cf.layout();
}
}
changingDepth--;
if (getLayouter() != null) {
Rectangle2D.Double bounds = getBounds();
Point2D.Double p = new Point2D.Double(bounds.x, bounds.y);
Rectangle2D.Double r = getLayouter().layout(
this, p, p);
setBounds(new Point2D.Double(r.x, r.y), new Point2D.Double(r.x + r.width, r.y + r.height));
invalidate();
}
}
/**
* Set a Layouter object which encapsulated a layout
* algorithm for this figure. Typically, a Layouter
* accesses the child components of this figure and arranges
* their graphical presentation. It is a good idea to set
* the Layouter in the protected initialize() method
* so it can be recreated if a GraphicalCompositeFigure is
* read and restored from a StorableInput stream.
*
*
* @param newLayouter encapsulation of a layout algorithm.
*/
@Override
public void setLayouter(Layouter newLayouter) {
this.layouter = newLayouter;
}
@Override
public Dimension2DDouble getPreferredSize() {
if (this.layouter != null) {
Rectangle2D.Double r = layouter.calculateLayout(this, getStartPoint(), getEndPoint());
return new Dimension2DDouble(r.width, r.height);
} else {
return super.getPreferredSize();
}
}
@Override
public void draw(Graphics2D g) {
Rectangle2D clipBounds = g.getClipBounds();
if (clipBounds != null) {
for (Figure child : getChildren()) {
if (child.isVisible() && child.getDrawingArea().intersects(clipBounds)) {
child.draw(g);
}
}
} else {
for (Figure child : getChildren()) {
if (child.isVisible()) {
child.draw(g);
}
}
}
}
@Override
public Collection<Figure> getDecomposition() {
LinkedList<Figure> list = new LinkedList<>();
list.add(this);
list.addAll(getChildren());
return list;
}
@Override
public void read(DOMInput in) throws IOException {
in.openElement("children");
for (int i = 0; i < in.getElementCount(); i++) {
basicAdd((Figure) in.readObject(i));
}
in.closeElement();
}
@Override
public void write(DOMOutput out) throws IOException {
out.openElement("children");
for (Figure child : getChildren()) {
out.writeObject(child);
}
out.closeElement();
}
@Override
public void restoreTransformTo(Object geometry) {
@SuppressWarnings("unchecked")
LinkedList<Object> list = (LinkedList<Object>) geometry;
Iterator<Object> i = list.iterator();
for (Figure child : getChildren()) {
child.restoreTransformTo(i.next());
}
invalidate();
}
@Override
public Object getTransformRestoreData() {
LinkedList<Object> list = new LinkedList<>();
for (Figure child : getChildren()) {
list.add(child.getTransformRestoreData());
}
return list;
}
@Override
public void basicAdd(int index, Figure figure) {
children.add(index, figure);
figure.addFigureListener(eventHandler);
}
@Override
public Figure basicRemoveChild(int index) {
Figure figure = children.remove(index);
figure.removeFigureListener(eventHandler);
invalidate();
return figure;
}
@Override
public java.util.List<Figure> getChildren() {
return Collections.unmodifiableList(children);
}
@Override
public int getChildCount() {
return children.size();
}
@Override
public Figure getChild(int index) {
return children.get(index);
}
@Override
public AbstractCompositeFigure clone() {
AbstractCompositeFigure that = (AbstractCompositeFigure) super.clone();
that.children = new ArrayList<>();
that.eventHandler = that.createEventHandler();
for (Figure thisChild : this.children) {
Figure thatChild = thisChild.clone();
that.children.add(thatChild);
thatChild.addFigureListener(that.eventHandler);
}
return that;
}
@Override
protected void validate() {
super.validate();
layout();
}
@Override
protected void invalidate() {
cachedBounds = null;
cachedDrawingArea = null;
}
@Override
public void willChange() {
super.willChange();
for (Figure child : children) {
child.willChange();
}
}
@Override
public void changed() {
for (Figure child : children) {
child.changed();
}
super.changed();
}
@Override
public int basicRemove(Figure child) {
int index = children.indexOf(child);
if (index != -1) {
basicRemoveChild(index);
}
return index;
}
@Override
public int indexOf(Figure child) {
return children.indexOf(child);
}
@Override
public Rectangle2D.Double getDrawingArea() {
return getDrawingArea(1.0);
}
@Override
public Rectangle2D.Double getDrawingArea(double factor) {
if (cachedDrawingArea == null) {
if (getChildCount() == 0) {
cachedDrawingArea = new Rectangle2D.Double();
} else {
for (Figure f : children) {
if (cachedDrawingArea == null) {
cachedDrawingArea = f.getDrawingArea(factor);
} else {
cachedDrawingArea.add(f.getDrawingArea(factor));
}
}
}
}
return (Rectangle2D.Double) cachedDrawingArea.clone();
}
@Override
public Rectangle2D.Double getBounds() {
if (cachedBounds == null) {
if (getChildCount() == 0) {
cachedBounds = new Rectangle2D.Double();
} else {
for (Figure f : children) {
if (cachedBounds == null) {
cachedBounds = f.getBounds();
} else {
cachedBounds.add(f.getBounds());
}
}
}
}
return (Rectangle2D.Double) cachedBounds.clone();
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureAdded(Figure f, int zIndex) {
CompositeFigureEvent 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] == CompositeFigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new CompositeFigureEvent(this, f, f.getDrawingArea(), zIndex);
}
((CompositeFigureListener) listeners[i + 1]).figureAdded(event);
}
}
}
/**
* Notify all listenerList that have registered interest for
* notification on this event type.
*/
protected void fireFigureRemoved(Figure f, int zIndex) {
CompositeFigureEvent 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] == CompositeFigureListener.class) {
// Lazily create the event:
if (event == null) {
event = new CompositeFigureEvent(this, f, f.getDrawingArea(), zIndex);
}
((CompositeFigureListener) listeners[i + 1]).figureRemoved(event);
}
}
}
@Override
public void removeCompositeFigureListener(CompositeFigureListener listener) {
listenerList.remove(CompositeFigureListener.class, listener);
}
@Override
public void addCompositeFigureListener(CompositeFigureListener listener) {
listenerList.add(CompositeFigureListener.class, listener);
}
}