package com.kreative.paint.document.draw; import java.awt.Composite; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import com.kreative.paint.document.undo.Atom; import com.kreative.paint.document.undo.History; import com.kreative.paint.document.undo.Recordable; public abstract class DrawObject implements Cloneable, Recordable { protected PaintSettings ps; protected AffineTransform tx; protected boolean visible; protected boolean locked; protected boolean selected; protected History history; protected List<DrawObjectListener> listeners; protected DrawObject(PaintSettings ps) { this.ps = ps; this.tx = null; this.visible = true; this.locked = false; this.selected = false; this.history = null; this.listeners = new ArrayList<DrawObjectListener>(); } protected DrawObject(DrawObject original) { this.ps = original.ps; this.tx = original.tx; this.visible = original.visible; this.locked = original.locked; this.selected = original.selected; this.history = null; this.listeners = new ArrayList<DrawObjectListener>(); } @Override public History getHistory() { return history; } @Override public void setHistory(History history) { this.history = history; } public void addDrawObjectListener(DrawObjectListener l) { listeners.add(l); } public void removeDrawObjectListener(DrawObjectListener l) { listeners.remove(l); } public DrawObjectListener[] getDrawObjectListeners() { return listeners.toArray(new DrawObjectListener[listeners.size()]); } protected void notifyDrawObjectListeners(int id) { if (listeners.isEmpty()) return; DrawObjectEvent e = new DrawObjectEvent(id, this); switch (id) { case DrawObjectEvent.DRAW_OBJECT_PAINT_SETTINGS_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectPaintSettingsChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_TRANSFORM_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectTransformChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_VISIBLE_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectVisibleChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_LOCKED_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectLockedChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_SELECTED_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectSelectedChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_CONTROL_POINT_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectControlPointChanged(e); break; case DrawObjectEvent.DRAW_OBJECT_LOCATION_CHANGED: for (DrawObjectListener l : listeners) l.drawObjectLocationChanged(e); break; default: for (DrawObjectListener l : listeners) l.drawObjectImplementationPropertyChanged(e); break; } } public PaintSettings getPaintSettings() { return ps; } private static class PaintSettingsAtom implements Atom { private DrawObject d; private PaintSettings oldPs; private PaintSettings newPs; public PaintSettingsAtom(DrawObject d, PaintSettings newPs) { this.d = d; this.oldPs = d.ps; this.newPs = newPs; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof PaintSettingsAtom) && (((PaintSettingsAtom)prev).d == this.d); } @Override public Atom buildUpon(Atom prev) { this.oldPs = ((PaintSettingsAtom)prev).oldPs; return this; } @Override public void undo() { d.ps = oldPs; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_PAINT_SETTINGS_CHANGED); } @Override public void redo() { d.ps = newPs; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_PAINT_SETTINGS_CHANGED); } } public void setPaintSettings(PaintSettings ps) { if (equals(this.ps, ps)) return; if (history != null) history.add(new PaintSettingsAtom(this, ps)); this.ps = ps; this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_PAINT_SETTINGS_CHANGED); } public AffineTransform getTransform() { return tx; } private static class TransformAtom implements Atom { private DrawObject d; private AffineTransform oldTx; private AffineTransform newTx; public TransformAtom(DrawObject d, AffineTransform newTx) { this.d = d; this.oldTx = d.tx; this.newTx = newTx; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof TransformAtom) && (((TransformAtom)prev).d == this.d); } @Override public Atom buildUpon(Atom prev) { this.oldTx = ((TransformAtom)prev).oldTx; return this; } @Override public void undo() { d.tx = oldTx; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_TRANSFORM_CHANGED); } @Override public void redo() { d.tx = newTx; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_TRANSFORM_CHANGED); } } public void setTransform(AffineTransform tx) { if (tx != null && tx.isIdentity()) tx = null; if (equals(this.tx, tx)) return; if (history != null) history.add(new TransformAtom(this, tx)); this.tx = tx; this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_TRANSFORM_CHANGED); } public boolean isVisible() { return visible; } public boolean isLocked() { return locked; } public boolean isSelected() { return selected; } private static class VisibleAtom implements Atom { private DrawObject d; private boolean oldVisible; private boolean newVisible; public VisibleAtom(DrawObject d, boolean newVisible) { this.d = d; this.oldVisible = d.visible; this.newVisible = newVisible; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof VisibleAtom) && (((VisibleAtom)prev).d == this.d); } @Override public Atom buildUpon(Atom prev) { this.oldVisible = ((VisibleAtom)prev).oldVisible; return this; } @Override public void undo() { d.visible = oldVisible; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_VISIBLE_CHANGED); } @Override public void redo() { d.visible = newVisible; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_VISIBLE_CHANGED); } } private static class LockedAtom implements Atom { private DrawObject d; private boolean oldLocked; private boolean newLocked; public LockedAtom(DrawObject d, boolean newLocked) { this.d = d; this.oldLocked = d.locked; this.newLocked = newLocked; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof LockedAtom) && (((LockedAtom)prev).d == this.d); } @Override public Atom buildUpon(Atom prev) { this.oldLocked = ((LockedAtom)prev).oldLocked; return this; } @Override public void undo() { d.locked = oldLocked; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_LOCKED_CHANGED); } @Override public void redo() { d.locked = newLocked; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_LOCKED_CHANGED); } } private static class SelectedAtom implements Atom { private DrawObject d; private boolean oldSelected; private boolean newSelected; public SelectedAtom(DrawObject d, boolean newSelected) { this.d = d; this.oldSelected = d.selected; this.newSelected = newSelected; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof SelectedAtom) && (((SelectedAtom)prev).d == this.d); } @Override public Atom buildUpon(Atom prev) { this.oldSelected = ((SelectedAtom)prev).oldSelected; return this; } @Override public void undo() { d.selected = oldSelected; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_SELECTED_CHANGED); } @Override public void redo() { d.selected = newSelected; d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_SELECTED_CHANGED); } } public void setVisible(boolean visible) { if (this.visible == visible) return; if (history != null) history.add(new VisibleAtom(this, visible)); this.visible = visible; this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_VISIBLE_CHANGED); } public void setLocked(boolean locked) { if (this.locked == locked) return; if (history != null) history.add(new LockedAtom(this, locked)); this.locked = locked; this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_LOCKED_CHANGED); } public void setSelected(boolean selected) { if (this.selected == selected) return; if (history != null) history.add(new SelectedAtom(this, selected)); this.selected = selected; this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_SELECTED_CHANGED); } /** * Returns a clone of this DrawObject. The clone may be a shallow copy * or a deep copy, but this method should guarantee that mutations to * the cloned DrawObject will not affect the original DrawObject. * @return a clone of this DrawObject */ @Override public abstract DrawObject clone(); /** * Returns the boundary of this DrawObject, given as if no * transformation has been applied to the DrawObject. This * will be used mainly for adjusting transformations. * @return the boundary of this DrawObject */ protected abstract Shape getBoundaryImpl(); public Rectangle getPreTxBounds() { Shape ba = getBoundaryImpl(); if (ba == null) return null; return ba.getBounds(); } public Rectangle2D getPreTxBounds2D() { Shape ba = getBoundaryImpl(); if (ba == null) return null; return ba.getBounds2D(); } public Rectangle getBounds() { Shape ba = getBoundaryImpl(); if (ba == null) return null; if (tx == null) return ba.getBounds(); return tx.createTransformedShape(ba).getBounds(); } public Rectangle2D getBounds2D() { Shape ba = getBoundaryImpl(); if (ba == null) return null; if (tx == null) return ba.getBounds2D(); return tx.createTransformedShape(ba).getBounds2D(); } /** * Returns the area of a graphics context that will be painted on * by this DrawObject, given as if no transformation has been * applied to the DrawObject. This will be used mainly for hit * detection. It is acceptable for the returned area to be approximate. * @return the area of a graphics context that will be painted on */ protected Shape getHitAreaImpl() { return null; } /** * Returns the area of a graphics context that will be painted on * by this DrawObject, after all transformations are applied. * This will be used mainly for hit detection. It is acceptable * for the returned area to be approximate. * @param tx the transformation applied to the DrawObject * @return the area of a graphics context that will be painted on */ protected Shape getPostTxHitAreaImpl(AffineTransform tx) { Shape ha = getHitAreaImpl(); if (tx == null || ha == null) return ha; return tx.createTransformedShape(ha); } /** * Returns the area of a graphics context that will be painted on * by this DrawObject, after all transformations are applied. * Implementations of this class should override getHitAreaImpl() * or getPostTxHitAreaImpl() instead. * @return the area of a graphics context that will be painted on */ public Shape getHitArea() { return getPostTxHitAreaImpl(tx); } /** * Returns the area of a graphics context that will be painted on * by this DrawObject, after all transformations are applied. * Implementations of this class should override getHitAreaImpl() * or getPostTxHitAreaImpl() instead. * @param x an additional X coordinate translation to apply * @param y an additional Y coordinate translation to apply * @return the area of a graphics context that will be painted on */ public Shape getHitArea(double x, double y) { AffineTransform htx = new AffineTransform(); htx.translate(x, y); if (this.tx != null) htx.concatenate(this.tx); return getPostTxHitAreaImpl(htx); } /** * Returns the area of a graphics context that will be painted on * by this DrawObject, after all transformations are applied. * Implementations of this class should override getHitAreaImpl() * or getPostTxHitAreaImpl() instead. * @param tx an additional transformation to apply * @return the area of a graphics context that will be painted on */ public Shape getHitArea(AffineTransform tx) { AffineTransform htx = new AffineTransform(); if (tx != null) htx.concatenate(tx); if (this.tx != null) htx.concatenate(this.tx); return getPostTxHitAreaImpl(htx); } public boolean contains(double x, double y) { Shape ha = getHitArea(); if (ha == null) return false; return ha.contains(x, y); } public boolean contains(double x, double y, double width, double height) { Shape ha = getHitArea(); if (ha == null) return false; return ha.contains(x, y, width, height); } public boolean contains(Point2D p) { Shape ha = getHitArea(); if (ha == null) return false; return ha.contains(p); } public boolean contains(Rectangle2D r) { Shape ha = getHitArea(); if (ha == null) return false; return ha.contains(r); } public boolean intersects(double x, double y, double width, double height) { Shape ha = getHitArea(); if (ha == null) return false; return ha.intersects(x, y, width, height); } public boolean intersects(Rectangle2D r) { Shape ha = getHitArea(); if (ha == null) return false; return ha.intersects(r); } /** * Returns an opaque object representing all variables that can be * affected by modifying control points or the location of this * DrawObject. An object returned from this method may be passed to * setControlState() at a later time, for example when the Undo or * Redo command is invoked. Every DrawObject subclass which overrides * setControlPointImpl() or setLocationImpl() should also override * this method. * @return an opaque object to be passed to setControlState() */ protected abstract Object getControlState(); /** * Restores all variables affected by modifying control points or * the location of this DrawObject from an opaque object produced by * getControlState(). An object returned from getControlState() may be * passed to this method at a later time, for example when the Undo or * Redo command is invoked. Every DrawObject subclass which overrides * setControlPointImpl() or setLocationImpl() should also override * this method. * @param state an opaque object returned from getControlState() */ protected abstract void setControlState(Object state); private static class ControlStateAtom implements Atom { private DrawObject d; private Object oldState; private Object newState; private int eventID; public ControlStateAtom(DrawObject d, Object oldState, Object newState, int eventID) { this.d = d; this.oldState = oldState; this.newState = newState; this.eventID = eventID; } @Override public boolean canBuildUpon(Atom prev) { return (prev instanceof ControlStateAtom) && (((ControlStateAtom)prev).d == this.d) && (((ControlStateAtom)prev).eventID == this.eventID); } @Override public Atom buildUpon(Atom prev) { this.oldState = ((ControlStateAtom)prev).oldState; return this; } @Override public void redo() { d.setControlState(newState); d.notifyDrawObjectListeners(eventID); } @Override public void undo() { d.setControlState(oldState); d.notifyDrawObjectListeners(eventID); } } /** * Returns the number of control points on this DrawObject. * A control point can be any variable that affects the rendering * of a DrawObject that can be represented by a geometric point. * @return the number of control points */ public abstract int getControlPointCount(); /** * Returns a control point on this DrawObject. The position of * the control point is given as if no transformation has been * applied to the DrawObject. A control point can be any * variable that affects the rendering of a DrawObject * that can be represented by a geometric point. * @param i the index of a control point * @return the control point */ protected abstract ControlPoint getControlPointImpl(int i); /** * Returns a list of control points on this DrawObject. The * position of each control point is given as if no transformation * has been applied to the DrawObject. A control point can be any * variable that affects the rendering of a DrawObject that * can be represented by a geometric point. * @return a list of control points */ protected abstract List<ControlPoint> getControlPointsImpl(); /** * Returns a set of lines that should be drawn when control points are * drawn. For example, the line between the endpoint of a Bezier curve * and its control point. The endpoints of each line are given as if * no transformation has been applied to the DrawObject. * @return a set of lines */ protected abstract Collection<Line2D> getControlLinesImpl(); /** * Sets the position of a control point on this DrawObject. * The new position is given as if no transformation has been * applied to the DrawObject. In most cases, this method should * return the index which was passed in. * @param i the index of a control point * @param x the new X coordinate of the control point * @param y the new Y coordinate of the control point * @return the index of the control point */ protected abstract int setControlPointImpl(int i, double x, double y); /** * Returns a control point on this DrawObject, * transformed according to the transformation applied * to this DrawObject. Implementations of this class * should override getControlPointImpl() instead. * @param i the index of a control point * @return the control point */ public ControlPoint getControlPoint(int i) { ControlPoint cp = getControlPointImpl(i); if (tx == null || cp == null) return cp; tx.transform(cp, cp); return cp; } /** * Returns a list of control points on this DrawObject, * transformed according to the transformation applied * to this DrawObject. Implementations of this class * should override getControlPointsImpl() instead. * @return a list of control points */ public List<ControlPoint> getControlPoints() { List<ControlPoint> cpts = getControlPointsImpl(); if (cpts == null) return Collections.emptyList(); if (tx == null || cpts.isEmpty()) return cpts; List<ControlPoint> txCpts = new ArrayList<ControlPoint>(); for (ControlPoint cp : cpts) { tx.transform(cp, cp); txCpts.add(cp); } return txCpts; } /** * Returns a set of lines that should be drawn when control points * are drawn, transformed according to the transformation applied * to this DrawObject. Implementations of this class * should override getControlLinesImpl() instead. * @return a set of lines */ public Collection<Line2D> getControlLines() { Collection<Line2D> lines = getControlLinesImpl(); if (lines == null) return Collections.emptySet(); if (tx == null || lines.isEmpty()) return lines; Collection<Line2D> txLines = new HashSet<Line2D>(); for (Line2D line : lines) { Point2D p1 = line.getP1(); Point2D p2 = line.getP2(); tx.transform(p1, p1); tx.transform(p2, p2); txLines.add(new Line2D.Double(p1, p2)); } return txLines; } /** * Sets the position of a control point on this DrawObject. * The new position is given transformed according to the * transformation applied to this DrawObject. Implementations * of this class should override setControlPointImpl() instead. * @param i the index of a control point * @param x the new X coordinate of the control point * @param y the new Y coordinate of the control point * @return the index of the control point */ public int setControlPoint(int i, double x, double y) { if (tx != null) { try { Point2D p = new Point2D.Double(x, y); tx.inverseTransform(p, p); x = p.getX(); y = p.getY(); } catch (Exception e) { return i; } } if (history != null) { Object oldState = this.getControlState(); i = this.setControlPointImpl(i, x, y); Object newState = this.getControlState(); history.add(new ControlStateAtom(this, oldState, newState, DrawObjectEvent.DRAW_OBJECT_CONTROL_POINT_CHANGED)); } else { i = this.setControlPointImpl(i, x, y); } this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_CONTROL_POINT_CHANGED); return i; } /** * Sets the position of a control point on this DrawObject. * The new position is given transformed according to the * transformation applied to this DrawObject. Implementations * of this class should override setControlPointImpl() instead. * @param i the index of a control point * @param p the new location of the control point * @return the index of the control point */ public int setControlPoint(int i, Point2D p) { return setControlPoint(i, p.getX(), p.getY()); } /** * Returns the location of this DrawObject, given as if no * transformation has been applied to the DrawObject. * This may be a control point, but does not have to be. * The location point is never actually shown; it is used * only to translate the DrawObject. * @return the location of this DrawObject */ protected abstract Point2D getLocationImpl(); /** * Sets the location of this DrawObject. The new location * is given as if no transformation has been applied to the * DrawObject. Setting the location of a DrawObject should * result only in a translation; no other transformations or * mutations should occur. * @param x the new X coordinate of the location * @param y the new Y coordinate of the location */ protected abstract void setLocationImpl(double x, double y); /** * Returns the location of this DrawObject, * transformed according to the transformation applied * to this DrawObject. Implementations of this class * should override getLocationImpl() instead. * @return the location of this DrawObject */ public Point2D getLocation() { Point2D p = getLocationImpl(); if (tx == null || p == null) return p; tx.transform(p, p); return p; } /** * Sets the location of this DrawObject. The new location * is given transformed according to the transformation * applied to this DrawObject. Implementations of this * class should override setLocationImpl() instead. * @param x the new X coordinate of the location * @param y the new Y coordinate of the location */ public void setLocation(double x, double y) { if (tx != null) { try { Point2D p = new Point2D.Double(x, y); tx.inverseTransform(p, p); x = p.getX(); y = p.getY(); } catch (Exception e) { return; } } if (history != null) { Object oldState = this.getControlState(); this.setLocationImpl(x, y); Object newState = this.getControlState(); history.add(new ControlStateAtom(this, oldState, newState, DrawObjectEvent.DRAW_OBJECT_LOCATION_CHANGED)); } else { this.setLocationImpl(x, y); } this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_LOCATION_CHANGED); } /** * Sets the location of this DrawObject. The new location * is given transformed according to the transformation * applied to this DrawObject. Implementations of this * class should override setLocationImpl() instead. * @param p the new location */ public void setLocation(Point2D p) { setLocation(p.getX(), p.getY()); } /** * Paints this DrawObject to a graphics context, * as if no transformations have been applied * to the DrawObject. * @param g the graphics context */ protected void paintImpl(Graphics2D g) {} /** * Paints this DrawObject to a graphics context, * with all transformations applied to the DrawObject. * @param g the graphics context * @param tx the transformation applied to the DrawObject */ protected void preTxPaintImpl(Graphics2D g, AffineTransform tx) { if (tx != null) g.transform(tx); paintImpl(g); } /** * Paints this DrawObject to a graphics context, * with all transformations applied to the DrawObject. * Implementations of this class should override * paintImpl() or preTxPaintImpl() instead. * @param g the graphics context */ public void paint(Graphics2D g) { Paint p = g.getPaint(); Composite c = g.getComposite(); Stroke s = g.getStroke(); Font f = g.getFont(); RenderingHints h = g.getRenderingHints(); AffineTransform gtx = g.getTransform(); preTxPaintImpl(g, tx); g.setTransform(gtx); g.setPaint(p); g.setComposite(c); g.setStroke(s); g.setFont(f); g.setRenderingHints(h); } /** * Paints this DrawObject to a graphics context, * with all transformations applied to the DrawObject. * Implementations of this class should override * paintImpl() or preTxPaintImpl() instead. * @param g the graphics context * @param x an additional X coordinate translation to apply * @param y an additional Y coordinate translation to apply */ public void paint(Graphics2D g, int x, int y) { Paint p = g.getPaint(); Composite c = g.getComposite(); Stroke s = g.getStroke(); Font f = g.getFont(); RenderingHints h = g.getRenderingHints(); AffineTransform gtx = g.getTransform(); AffineTransform dtx = new AffineTransform(); dtx.translate(x, y); if (this.tx != null) dtx.concatenate(this.tx); preTxPaintImpl(g, dtx); g.setTransform(gtx); g.setPaint(p); g.setComposite(c); g.setStroke(s); g.setFont(f); g.setRenderingHints(h); } /** * Paints this DrawObject to a graphics context, * with all transformations applied to the DrawObject. * Implementations of this class should override * paintImpl() or preTxPaintImpl() instead. * @param g the graphics context * @param tx an additional transformation to apply */ public void paint(Graphics2D g, AffineTransform tx) { Paint p = g.getPaint(); Composite c = g.getComposite(); Stroke s = g.getStroke(); Font f = g.getFont(); RenderingHints h = g.getRenderingHints(); AffineTransform gtx = g.getTransform(); AffineTransform dtx = new AffineTransform(); if (tx != null) dtx.concatenate(tx); if (this.tx != null) dtx.concatenate(this.tx); preTxPaintImpl(g, dtx); g.setTransform(gtx); g.setPaint(p); g.setComposite(c); g.setStroke(s); g.setFont(f); g.setRenderingHints(h); } private static boolean equals(Object dis, Object dat) { if (dis == null) return (dat == null); if (dat == null) return (dis == null); return dis.equals(dat); } }